[Scummvm-git-logs] scummvm master -> 1fd96b5085b230ab9c30d50a178eeafa512a22d7

lephilousophe noreply at scummvm.org
Sun Apr 12 15:28:29 UTC 2026


This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
0c78399e8a AUDIO: Allow adjust of output buffer size
1fd96b5085 ANDROID: Rework the audio subsystem and use Oboe


Commit: 0c78399e8ac537fd7717d6e56c1646b3eb1a69d6
    https://github.com/scummvm/scummvm/commit/0c78399e8ac537fd7717d6e56c1646b3eb1a69d6
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-04-12T17:28:26+02:00

Commit Message:
AUDIO: Allow adjust of output buffer size

When the audio output is switched, the optimum output size may change

Changed paths:
    audio/mixer_intern.h


diff --git a/audio/mixer_intern.h b/audio/mixer_intern.h
index fea60437a33..409315c3c6f 100644
--- a/audio/mixer_intern.h
+++ b/audio/mixer_intern.h
@@ -65,7 +65,7 @@ private:
 
 	const uint _sampleRate;
 	const bool _stereo;
-	const uint _outBufSize;
+	uint _outBufSize;
 	bool _mixerReady;
 	uint32 _handleSeed;
 
@@ -144,6 +144,11 @@ protected:
 	void insertChannel(SoundHandle *handle, Channel *chan);
 
 public:
+	/**
+	 * Adjust the output buffer size
+	 */
+	void setOutputBufSize(uint outBufSize) { _outBufSize = outBufSize; }
+
 	/**
 	 * The mixer callback function, to be called at regular intervals by
 	 * the backend (e.g. from an audio mixing thread). All the actual mixing


Commit: 1fd96b5085b230ab9c30d50a178eeafa512a22d7
    https://github.com/scummvm/scummvm/commit/1fd96b5085b230ab9c30d50a178eeafa512a22d7
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-04-12T17:28:26+02:00

Commit Message:
ANDROID: Rework the audio subsystem and use Oboe

It would appear that Oboe is the new audio library.
It's way more complex to handle (especially on really old devices), but
it brings low latency.
Oboe also comes with its share of warkaround to apply that have no
real doc... A fine piece of software...

Changed paths:
  A backends/mixer/android/android-mixer.cpp
  A backends/mixer/android/android-mixer.h
  A backends/mixer/android/ringbuffer.h
    backends/module.mk
    backends/platform/android/android.cpp
    backends/platform/android/android.h
    backends/platform/android/jni-android.cpp
    backends/platform/android/jni-android.h
    backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
    backends/platform/android/org/scummvm/scummvm/ScummVM.java
    backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
    configure


diff --git a/backends/mixer/android/android-mixer.cpp b/backends/mixer/android/android-mixer.cpp
new file mode 100644
index 00000000000..61a57afe21c
--- /dev/null
+++ b/backends/mixer/android/android-mixer.cpp
@@ -0,0 +1,484 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Allow use of stuff in <time.h>
+#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
+
+#include <sys/time.h>
+#include <time.h>
+
+#include <android/log.h>
+#include <oboe/Oboe.h>
+
+#include "backends/mixer/android/ringbuffer.h"
+
+#include "backends/mixer/android/android-mixer.h"
+#include "backends/platform/android/android.h"
+#include "backends/platform/android/jni-android.h"
+
+//#define OBOE_DEBUG
+
+extern const char *android_log_tag;
+
+class AndroidMixerManagerImpl : public AndroidMixerManager, public oboe::AudioStreamDataCallback {
+public:
+	AndroidMixerManagerImpl() : _chunkSize(0), _buffer(nullptr), _stream(nullptr), _latency(nullptr), _audio_thread_exit(false), _underflows(0) {}
+	~AndroidMixerManagerImpl() override;
+
+	void init() override;
+	void signalQuit() override { _audio_thread_exit = true; }
+	void quit() override;
+
+	void notifyAudioDisconnect() { _plugEvent.store(true); }
+private:
+	oboe::DataCallbackResult onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) override;
+
+	void initStream();
+	static void *audioThreadFunc(void *arg);
+
+	// We only support 16-bits interleaved stereo streams
+	static constexpr size_t kChannelCount = 2;
+	typedef int16 frame_t[kChannelCount];
+
+	size_t _chunkSize;
+
+	RingBuffer<frame_t> *_buffer;
+	std::shared_ptr<oboe::AudioStream> _stream_ptr;
+	oboe::LatencyTuner *_latency;
+
+	bool _audio_thread_exit;
+	pthread_t _audio_thread;
+
+	// LLVM between 8 and 10 wrongfully said they supported this
+#if 0 && __cpp_lib_hardware_interference_size
+	static constexpr size_t hardware_destructive_interference_size = std::hardware_destructive_interference_size;
+#else
+	// 64 is a good fit for most platforms
+	static constexpr size_t hardware_destructive_interference_size = 64;
+#endif
+
+	// Written in mixing thread, read in callback
+	// As there is no atomic support for shared_ptr before C++20, copy the pointer here to allow for checking in callback
+	alignas(hardware_destructive_interference_size) std::atomic<oboe::AudioStream *> _stream;
+	// Written in data callback, read in mixing thread and data callback
+	alignas(hardware_destructive_interference_size) std::atomic<size_t> _underflows;
+	// Written in Android threads, read/written in mixing thread
+	alignas(hardware_destructive_interference_size) std::atomic<bool> _plugEvent;
+};
+
+AndroidMixerManager *AndroidMixerManager::make() {
+	return new AndroidMixerManagerImpl();
+}
+
+AndroidMixerManagerImpl::~AndroidMixerManagerImpl() {
+	delete _latency;
+	_stream_ptr.reset();
+	delete _buffer;
+}
+
+void AndroidMixerManagerImpl::init() {
+	initStream();
+
+	// Our mixer code does IO while rendering.
+	// This is forbidden for Oboe callbacks: use another thread for it.
+	_audio_thread_exit = false;
+	pthread_create(&_audio_thread, 0, audioThreadFunc, this);
+}
+
+void AndroidMixerManagerImpl::quit() {
+	pthread_join(_audio_thread, 0);
+
+	LOGD("Stopping audio stream");
+	_stream_ptr->stop();
+
+	LOGD("Closing audio stream");
+	_stream_ptr->close();
+
+	// After this point, the stream is expected to be unused
+
+	LOGD("Cleanup audio stream");
+
+	delete _latency;
+	_latency = nullptr;
+
+	_stream_ptr.reset();
+}
+
+void AndroidMixerManagerImpl::initStream() {
+	oboe::AudioStreamBuilder builder;
+	std::shared_ptr<oboe::AudioStream> stream;
+
+	stream.swap(_stream_ptr);
+
+	if (stream) {
+		LOGD("Delete old audio stream");
+		stream->close();
+
+		delete _latency;
+		_latency = nullptr;
+		// After this point, the stream is expected to be unused
+
+		stream.reset();
+	}
+
+	builder.setDirection(oboe::Direction::Output)
+		//->setAudioApi(oboe::AudioApi::OpenSLES)
+		->setSharingMode(oboe::SharingMode::Exclusive)
+		->setPerformanceMode(oboe::PerformanceMode::LowLatency)
+		->setChannelConversionAllowed(true)
+		->setChannelCount(oboe::ChannelCount::Stereo)
+		->setFormatConversionAllowed(true)
+		->setFormat(oboe::AudioFormat::I16)
+		->setDataCallback(this);
+
+	if (_mixer) {
+		// We can't tear down our mixer when started.
+		// So, reuse its sample rate
+		LOGD("Reuse old mixer rate: %d", _mixer->getOutputRate());
+		builder.setSampleRate(_mixer->getOutputRate());
+		// Let's stick to Oboe's default of Medium quality resampling
+	}
+
+	oboe::Result result = builder.openStream(stream);
+	if (result != oboe::Result::OK) {
+		// Calling log_assert allows us to get the error message reported through Google
+		// This aborts.
+		__android_log_assert(nullptr, android_log_tag,
+		                     "Failed to create audio stream. Error: %s",
+		                     oboe::convertToText(result));
+	}
+
+	oboe::AudioApi streamApi = stream->getAudioApi();
+	LOGD("Got new stream %s -> %p", oboe::convertToText(streamApi), stream.get());
+
+	int32_t sampleRate = stream->getSampleRate();
+	size_t chunkSize = stream->getFramesPerBurst();
+
+	// Configure the RingBuffer using full buffer capacity to make sure we can provide the requested samples
+	size_t bufferCapacity = stream->getBufferCapacityInFrames();
+
+	if (streamApi == oboe::AudioApi::OpenSLES) {
+		// With OpenSL ES, Oboe has 2 buffers of size defined by Java (AudioTrack or AudioManager)
+		// This is not really enough so we can get several consecutive callbacks which drain the ring buffer
+		// Instead of using bigger bursts, increase here the ring buffer size up to ~150ms
+		size_t minCapacity = sampleRate / 6;
+		if (bufferCapacity < minCapacity) {
+			bufferCapacity = minCapacity;
+		}
+	}
+
+	if (!_buffer) {
+		LOGD("Setting up ring buffer with capacity: %zu", bufferCapacity);
+		_buffer = new RingBuffer<frame_t>(bufferCapacity);
+	} else if (_chunkSize != chunkSize) {
+		LOGD("Reconfiguring ring buffer with capacity: %zu", bufferCapacity);
+		RingBuffer<frame_t> *old_buffer = _buffer;
+		_buffer = new RingBuffer<frame_t>(bufferCapacity, std::move(*old_buffer));
+		delete old_buffer;
+	}
+
+	if (!_mixer) {
+		LOGD("Setting up mixer with settings sample rate: %d and buffer: %zu", sampleRate, chunkSize);
+		_mixer = new Audio::MixerImpl(sampleRate, kChannelCount, chunkSize);
+		_mixer->setReady(true);
+	} else if (_chunkSize != chunkSize) {
+		LOGD("Reconfiguring mixer with buffer %zu", chunkSize);
+		_mixer->setOutputBufSize(chunkSize);
+	}
+
+	stream->setBufferSizeInFrames(chunkSize);
+	_chunkSize = chunkSize;
+
+	_latency = new oboe::LatencyTuner(*stream);
+
+	_stream_ptr.swap(stream);
+	LOGD("Finished initStream");
+}
+
+
+oboe::DataCallbackResult AndroidMixerManagerImpl::onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) {
+	if (audioStream != _stream.load()) {
+		// That's not our stream: stop here
+		return oboe::DataCallbackResult::Stop;
+	}
+
+#ifdef OBOE_DEBUG
+	const int32_t numFrames_ = numFrames;
+#endif
+
+	frame_t *outputData = static_cast<frame_t *>(audioData);
+
+	while (numFrames > 0) {
+		size_t n = numFrames;
+		frame_t *inputData = _buffer->try_consume(&n);
+		if (!inputData) {
+			break;
+		}
+		memcpy(outputData, inputData, n * sizeof(frame_t));
+		_buffer->consumed();
+		numFrames -= n;
+		outputData += n;
+		_underflows.store(0, std::memory_order_release);
+	}
+#ifdef OBOE_DEBUG
+	LOGD("onAudioReady: numFrames=%d/%d => underflows=%zu", numFrames, numFrames_, _underflows.load(std::memory_order_relaxed));
+#endif
+	if (numFrames > 0) {
+		memset(outputData, 0, numFrames * sizeof(frame_t));
+
+		size_t underflows = _underflows.load(std::memory_order_relaxed);
+		underflows += numFrames;
+		if (underflows < 0x80000000) {
+			_underflows.store(underflows, std::memory_order_release);
+		}
+	}
+
+	_latency->tune();
+	return oboe::DataCallbackResult::Continue;
+}
+
+static void samples_to_ts(struct timespec *ts, uint64 samples, uint64 sampleRate) {
+	static constexpr uint64 kNS = 1000000000; // 1s == 1e9ns
+
+	const uint64 ns = samples * kNS / sampleRate;
+	ts->tv_sec  = ns / kNS;
+	ts->tv_nsec = ns % kNS;
+}
+
+static int64_t diff_ts_ns(const struct timespec *tv_a, const struct timespec *tv_b) {
+	static constexpr uint64 kNS = 1000000000; // 1s == 1e9ns
+
+	return (int64_t)(tv_a->tv_sec - tv_b->tv_sec) * kNS +
+		        (tv_a->tv_nsec - tv_b->tv_nsec);
+}
+
+void *AndroidMixerManagerImpl::audioThreadFunc(void *arg) {
+	AndroidMixerManagerImpl *this_ = (AndroidMixerManagerImpl *)arg;
+	Audio::MixerImpl *mixer = this_->_mixer;
+
+	std::shared_ptr<oboe::AudioStream> stream = this_->_stream_ptr;
+	this_->_stream.store(stream.get());
+
+	int32_t sampleRate = stream->getSampleRate();
+
+	// Wait for 2 secs of silence before pausing the stream
+	const size_t silencePause = sampleRate * 2;
+	size_t silence_count = 0;
+
+	struct timespec tv_chunk, tv_silence;
+	// We try to fill half by half
+	size_t chunkSize = this_->_chunkSize / 2;
+	samples_to_ts(&tv_chunk, chunkSize, sampleRate);
+
+	// Variables of Android P/Q worakaround for AAudio
+	bool plugWaiting = false;
+	struct timespec tv_plugEvent, tv_recreated;
+
+#define STREAM_RECREATE()                                                      \
+	do {                                                                   \
+		stream.reset();                                                \
+		this_->_stream.store(nullptr);                                 \
+		this_->initStream();                                           \
+		/* Refresh our local copy and synchronize for callback */      \
+		stream = this_->_stream_ptr;                                   \
+		this_->_stream.store(stream.get());                            \
+		chunkSize = this_->_chunkSize / 2;                             \
+		samples_to_ts(&tv_chunk, chunkSize, stream->getSampleRate());  \
+		clock_gettime(CLOCK_MONOTONIC, &tv_recreated);                 \
+	} while (false)
+
+
+	while (!this_->_audio_thread_exit) {
+		const oboe::StreamState state = stream->getState();
+		const bool started = (state == oboe::StreamState::Starting ||
+				state == oboe::StreamState::Started);
+		if (JNI::pause) {
+			if (started) {
+				LOGD("Pausing audio stream");
+				stream->requestPause();
+			}
+
+			LOGD("audio thread going to sleep");
+			sem_wait(&JNI::pause_sem);
+			LOGD("audio thread woke up");
+
+			if (started) {
+				LOGD("Restarting audio stream");
+				oboe::Result result = stream->start();
+				if (result != oboe::Result::OK) {
+					// Oops something went wrong: recreate the stream
+					STREAM_RECREATE();
+				}
+			}
+
+			// Redo to refresh the state
+			continue;
+		}
+
+		// begin Android P/Q workaround for AAudio not detection disconnections
+		if (plugWaiting) {
+			timespec tv_now;
+			clock_gettime(CLOCK_MONOTONIC, &tv_now);
+
+			if (diff_ts_ns(&tv_now, &tv_plugEvent) > 3000000000) {
+				// timeout is now elapsed, check if a recreate happened since the first plug event
+				plugWaiting = false;
+
+				bool recreate = diff_ts_ns(&tv_recreated, &tv_plugEvent) < 0;
+				LOGI("Plug event timeout elpased: will recreate=%d", recreate);
+				if (recreate) {
+					stream->stop();
+					STREAM_RECREATE();
+				}
+				continue;
+			}
+		}
+		if (this_->_plugEvent.exchange(false, std::memory_order_acquire)) {
+			LOGI("Got a plug/unplug event from Java");
+			if (!plugWaiting) {
+				if (stream->usesAAudio() && oboe::OboeExtensions::isMMapUsed(stream.get())) {
+					// Register the event time
+					clock_gettime(CLOCK_MONOTONIC, &tv_plugEvent);
+
+					if (diff_ts_ns(&tv_plugEvent, &tv_recreated) > 1000000000) {
+						// The last recreate happened more than 1s before: that's not tied
+						plugWaiting = true;
+						// leave some iterations to get a disconnect from Oboe
+						if (!started) {
+							// Force a start to detect the disconnection
+							// We will pause at the next loop
+							oboe::Result result = stream->start();
+							if (result != oboe::Result::OK) {
+								// Oops something went wrong: recreate the stream
+								STREAM_RECREATE();
+							}
+							continue;
+						}
+					} else {
+						LOGI("Plug event already catched by Oboe");
+					}
+				} else {
+					LOGI("Not a MMap stream: no workaround needed => ignoring");
+				}
+			}
+		}
+		// end Android P/Q workaround for AAudio not detection disconnections
+
+		size_t req = chunkSize;
+		frame_t *outputData = this_->_buffer->try_produce(&req);
+		if (!outputData) {
+			// buffer is full: either we have to wait for a new slot or to restart the stream
+			if (started) {
+#ifdef OBOE_DEBUG
+				LOGD("Waiting for buffer space for %ld.%09ld", tv_chunk.tv_sec, tv_chunk.tv_nsec);
+#endif
+				nanosleep(&tv_chunk, nullptr);
+				continue;
+			}
+
+			oboe::Result result = stream->start();
+			if (result != oboe::Result::OK) {
+				// Oops something went wrong: recreate the stream
+				STREAM_RECREATE();
+				// Don't restart in this case to let the time for the buffer to fill
+			}
+
+			// Stream is restarted: let's try again
+			continue;
+		}
+
+		// mixCallback returns a number of frames although we pass to it a buffer size in bytes
+		size_t n = mixer->mixCallback((byte *)outputData, req * sizeof(frame_t));
+
+		// check if the burst is full of silence so we can shut the stream
+		bool silence = true;
+		for (size_t i = 0; i < n; i++) {
+			// SID streams constant crap
+			if (outputData[i][0] > 32 || outputData[i][1] > 32) {
+				silence = false;
+				break;
+			}
+		}
+		if (silence) {
+			// The whole chunk was silence: add it to our count
+			silence_count += n;
+			if (silence_count > 0x80000000) {
+				silence_count = 0x80000000;
+			}
+			if (!started) {
+				// if the stream is not playing, don't feed it with silence: that would make it start
+				n = 0;
+			}
+		} else {
+			silence_count = 0;
+		}
+
+		// If the mixer didn't produce any data (because there is no stream), the buffer will starve.
+		// The callback will then fill with placeholder silence and increase the underflow count.
+		// We will then pause the stream below.
+		this_->_buffer->produced(n);
+
+#ifdef OBOE_DEBUG
+		if (n > 0) {
+			LOGD("Requested mix of %zu frames and got %zu%s - Buffer size: %d / %d", req, n, silence ? " S" : "", stream->getBufferSizeInFrames(), stream->getBufferCapacityInFrames());
+		}
+#endif
+
+		if (silence && started) {
+			// nothing was produced
+			size_t underflows = this_->_underflows.load(std::memory_order_acquire);
+			if (underflows + silence_count >= silencePause) {
+				LOGD("Silence %zu+%zu: pausing audio stream", underflows, silence_count);
+				stream->pause();
+				stream->requestFlush();
+			}
+		}
+
+		if (n == 0) {
+			// we either didn't produce anything or produced silence while stopped
+			// wait as if we pushed it
+			samples_to_ts(&tv_silence, req, sampleRate);
+			nanosleep(&tv_silence, nullptr);
+		}
+	}
+
+#undef STREAM_RECREATE
+
+	this_->_stream.store(nullptr);
+	return 0;
+}
+
+void JNI::setDefaultAudioValues(JNIEnv *env, jclass clazz, jint sampleRate, jint framesPerBurst) {
+	LOGD("Default audio values are sr=%d fpb=%d", sampleRate, framesPerBurst);
+	oboe::DefaultStreamValues::SampleRate = sampleRate;
+	oboe::DefaultStreamValues::FramesPerBurst = framesPerBurst;
+}
+
+void JNI::notifyAudioDisconnect(JNIEnv *env, jclass clazz) {
+	if (!g_system) {
+		return;
+	}
+	MixerManager *mm = dynamic_cast<OSystem_Android *>(g_system)->getMixerManager();
+	if (!mm) {
+		return;
+	}
+	dynamic_cast<AndroidMixerManagerImpl *>(mm)->notifyAudioDisconnect();
+}
diff --git a/backends/mixer/android/android-mixer.h b/backends/mixer/android/android-mixer.h
new file mode 100644
index 00000000000..d208f7c5bc6
--- /dev/null
+++ b/backends/mixer/android/android-mixer.h
@@ -0,0 +1,38 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef _BACKENDS_MIXER_ANDROID_MIXER_H_
+#define _BACKENDS_MIXER_ANDROID_MIXER_H_
+
+#include "backends/mixer/mixer.h"
+
+class AndroidMixerManager : public MixerManager {
+public:
+	static AndroidMixerManager *make();
+	virtual void signalQuit() = 0;
+	virtual void quit() = 0;
+
+protected:
+	void suspendAudio() override { }
+	int resumeAudio() override { return -2; }
+};
+
+#endif
diff --git a/backends/mixer/android/ringbuffer.h b/backends/mixer/android/ringbuffer.h
new file mode 100644
index 00000000000..afd9ba6b8fa
--- /dev/null
+++ b/backends/mixer/android/ringbuffer.h
@@ -0,0 +1,265 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef _BACKENDS_MIXER_ANDROID_RINGBUFFER_H_
+#define _BACKENDS_MIXER_ANDROID_RINGBUFFER_H_
+
+#include <assert.h>
+#include <stddef.h>
+#include <string.h>
+#include <new>
+#include <atomic>
+
+/**
+ * A lock-free FIFO ring-buffer with contiguous buffers for production.
+ */
+template<typename T>
+class RingBuffer {
+public:
+	RingBuffer(size_t n) : _size(n + 1), _buffer(new T[_size]), _pending_read(0), _pending_write(0), _read(0), _write(0), _last(0) { }
+	RingBuffer(const RingBuffer<T> &) = delete;
+
+	/*
+	 * Contruct a new RingBuffer moving data from the old one
+	 * The other RingBuffer must not be in use and destroyed afterwards.
+	 *
+	 * When the new RingBuffer is smaller than the previous one, the older samples are dropped.
+	 */
+	RingBuffer(size_t n, RingBuffer<T> &&o) : RingBuffer(n) {
+		// First make the RingBuffer was in a valid state and make it invalid
+		const size_t write = o._write.exchange(-1);
+		const size_t read = o._read.exchange(-1);
+		assert(o._pending_write == write);
+		o._pending_write = 0;
+		assert(o._pending_read == read);
+		o._pending_read = -1;
+
+		T const *buffer = o._buffer;
+		o._buffer = nullptr;
+
+		// From here, o is completely invalid
+
+		if (read == write) {
+			// Empty queue: nothing to move
+			delete[] buffer;
+			return;
+		}
+		if (read < write) {
+			// Cap the kept data to our own buffer size
+			size_t nread = read;
+			if (nread + n < write) {
+				nread = write - n;
+			}
+			_pending_write = write - nread;
+
+			memcpy(&_buffer[0], &buffer[nread], _pending_write * sizeof(T));
+
+			_last.store(_pending_write, std::memory_order_relaxed);
+			_write.store(_pending_write, std::memory_order_release);
+			delete[] buffer;
+			return;
+		}
+
+		// read > write: the buffer is in two parts
+		if (n <= write) {
+			// Easy: we can take the last n samples in one shot
+			_pending_write = n;
+			memcpy(&_buffer[0], &buffer[write - n], n * sizeof(T));
+
+			_last.store(_pending_write, std::memory_order_relaxed);
+			_write.store(_pending_write, std::memory_order_release);
+
+			delete[] buffer;
+			return;
+		}
+
+		// n > write
+		size_t last = o._last.load(std::memory_order_relaxed);
+		size_t end_part_sz = last - read;
+		if (end_part_sz > (n - write)) {
+			end_part_sz = n - write;
+		}
+
+		// First, copy the end of the buffer up to last, then copy the whole beginning
+		_pending_write = end_part_sz + write;
+		memcpy(&_buffer[0], &buffer[last - end_part_sz], end_part_sz * sizeof(T));
+		memcpy(&_buffer[end_part_sz], &buffer[0], write * sizeof(T));
+
+		_last.store(_pending_write, std::memory_order_relaxed);
+		_write.store(_pending_write, std::memory_order_release);
+
+		delete[] buffer;
+	}
+
+	~RingBuffer() { delete[] _buffer; }
+
+	/*
+	 * Try to produce at least n elements.
+	 * The ring-buffer will adjust n with the real element count
+	 * which should be produced.
+	 * In case of failure, nullptr is returned.
+	 *
+	 * When successful, n is guaranteed to be at least what has been queried.
+	 * A pointer to the buffer to fill is returned.
+	 */
+	T *try_produce(size_t *n) {
+		size_t real_n = *n;
+		assert(real_n > 0);
+
+		size_t write = _write.load(std::memory_order_relaxed);
+		size_t read = _read.load(std::memory_order_acquire);
+		assert(_pending_write == write);
+
+		// Try to acquire at at least real_n records
+		if (read <= write) {
+			if (write + real_n <= _size) {
+				real_n = _size - write;
+				*n = real_n;
+				_wraparound_write = false;
+				_pending_write = write + real_n;
+				return &_buffer[write];
+			} else if (real_n < read) { // Don't go up to read: that would make believe it's empty
+				real_n = read - 1;
+				*n = real_n;
+				_wraparound_write = true;
+				_pending_write = real_n;
+				return &_buffer[0];
+			} else {
+				return nullptr;
+			}
+		} else {
+			if (write + real_n < read) { // Don't go up to read: that would make believe it's empty
+				real_n = read - write - 1;
+				*n = real_n;
+				_wraparound_write = false;
+				_pending_write = write + real_n;
+				return &_buffer[write];
+			} else {
+				return nullptr;
+			}
+		}
+	}
+
+	/*
+	 * Indicate that n samples have been produced.
+	 * n must be less than or equal to what have been returned by try_produce.
+	 */
+	void produced(size_t n) {
+		size_t write = _write.load(std::memory_order_relaxed);
+		size_t pending_write;
+		if (_wraparound_write) {
+			pending_write = n;
+			_last.store(write, std::memory_order_relaxed);
+		} else {
+			pending_write = write + n;
+		}
+		// Make sure we didn't overshoot
+		assert(_pending_write >= pending_write);
+		if (pending_write > _last.load(std::memory_order_relaxed)) {
+			_last.store(pending_write, std::memory_order_relaxed);
+		}
+		_pending_write = pending_write;
+		_write.store(pending_write, std::memory_order_release);
+	}
+
+	/*
+	 * Try to consume at most n elements.
+	 * If there is less than n elements (or if the buffer is not contiguous), adjusts n to the real count.
+	 * If there is no element available, returns nullptr.
+	 *
+	 * Loop over try_consume until it returns nullptr to fetch all the expected elements.
+	 */
+	T *try_consume(size_t *n) {
+		size_t real_n = *n;
+		assert(real_n > 0);
+
+		size_t read = _read.load(std::memory_order_relaxed);
+		assert(_pending_read == read);
+
+		// Try to acquire at most n records
+		size_t write = _write.load(std::memory_order_acquire);
+
+		if (read == write) {
+			// Empty queue: nothing to return
+			return nullptr;
+		} else if (read < write) {
+			if (read + real_n > write) {
+				real_n = write - read;
+			}
+			*n = real_n;
+			_pending_read = read + real_n;
+			return &_buffer[read];
+		} else {
+			size_t last = _last.load(std::memory_order_relaxed);
+			if (read == last) { // This happens when we read up to the end the last time: consider read as 0
+				if (0 == write) {
+					// Empty queue: nothing to return
+					return nullptr;
+				}
+				if (real_n > write) {
+					real_n = write;
+				}
+				*n = real_n;
+				_pending_read = real_n;
+				return &_buffer[0];
+			} else if (read + real_n < last) {
+				*n = real_n;
+				_pending_read =  read + real_n;
+				return &_buffer[read];
+			} else {
+				*n = last - read;
+				_pending_read = 0;
+				return &_buffer[read];
+			}
+		}
+	}
+
+	/*
+	 * Indicate that the previous consume request has been done.
+	 *
+	 * Frees the buffer for more produced samples.
+	 */
+	void consumed() {
+		_read.store(_pending_read, std::memory_order_release);
+	}
+
+private:
+	const size_t _size;
+	T *_buffer;
+
+	size_t _pending_read;
+	size_t _pending_write;
+	bool   _wraparound_write;
+
+	// LLVM between 8 and 10 wrongfully said they supported this
+#if 0 && __cpp_lib_hardware_interference_size
+	static constexpr size_t hardware_destructive_interference_size = std::hardware_destructive_interference_size;
+#else
+	// 64 is a good fit for most platforms
+	static constexpr size_t hardware_destructive_interference_size = 64;
+#endif
+
+	alignas(hardware_destructive_interference_size) std::atomic<size_t> _read;
+	alignas(hardware_destructive_interference_size) std::atomic<size_t> _write;
+	alignas(hardware_destructive_interference_size) std::atomic<size_t> _last;
+};
+
+#endif
diff --git a/backends/module.mk b/backends/module.mk
index c61e2a910ee..f64a8112bcf 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -346,6 +346,7 @@ MODULE_OBJS += \
 	fs/android/android-posix-fs.o \
 	fs/android/android-saf-fs.o \
 	graphics/android/android-graphics.o \
+	mixer/android/android-mixer.o \
 	mutex/pthread/pthread-mutex.o \
 	networking/basic/android/jni.o \
 	networking/basic/android/socket.o \
@@ -356,6 +357,10 @@ MODULE_OBJS += \
 	networking/http/android/connectionmanager-android.o \
 	networking/http/android/networkreadstream-android.o
 endif
+
+# Oboe headers need C++14...
+$(MODULE)/mixer/android/android-mixer.o: CXXFLAGS += "-std=c++14"
+
 endif
 
 ifdef AMIGAOS
diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
index a0329adef21..f4e620081d0 100644
--- a/backends/platform/android/android.cpp
+++ b/backends/platform/android/android.cpp
@@ -64,8 +64,11 @@
 
 #include "backends/graphics/android/android-graphics.h"
 
+#include "backends/mixer/android/android-mixer.h"
+
 #include "backends/audiocd/default/default-audiocd.h"
 #include "backends/events/default/default-events.h"
+#include "backends/mixer/mixer.h"
 #include "backends/mutex/pthread/pthread-mutex.h"
 #include "backends/saves/default/default-saves.h"
 #include "backends/timer/default/default-timer.h"
@@ -181,13 +184,10 @@ public:
 	}
 };
 
-OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
-	_audio_sample_rate(audio_sample_rate),
-	_audio_buffer_size(audio_buffer_size),
+OSystem_Android::OSystem_Android() :
 	_screen_changeid(0),
 	_virtkeybd_on(false),
-	_mixer(0),
-	_event_queue_lock(0),
+	_event_queue_lock(nullptr),
 	_touch_pt_down(),
 	_touch_pt_scroll(),
 	_touch_pt_dt(),
@@ -243,22 +243,10 @@ OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
 
 OSystem_Android::~OSystem_Android() {
 	ENTER();
-	// _audiocdManager should be deleted before _mixer!
-	// It is normally deleted in proper order in the OSystem destructor.
-	// However, currently _mixer is deleted here (OSystem_Android)
-	// and in the ModularBackend destructor,
-	// hence unless _audiocdManager is deleted here first,
-	// it will cause a crash for the Android app (arm64 v8a) upon exit
-	// -- when the audio cd manager was actually used eg. audio cd test of the testbed
-	// FIXME: A more proper fix would probably be to:
-	//        - delete _mixer in the base class (OSystem) after _audiocdManager (this is already the current behavior)
-	//	      - remove its deletion from OSystem_Android and ModularBackend (this is what needs to be fixed).
-	delete _audiocdManager;
-	_audiocdManager = 0;
-	delete _mixer;
-	_mixer = 0;
+
 	_fsFactory = 0;
 	AndroidFilesystemFactory::destroy();
+
 	delete _timerManager;
 	_timerManager = 0;
 
@@ -305,131 +293,6 @@ void *OSystem_Android::timerThreadFunc(void *arg) {
 	return 0;
 }
 
-void *OSystem_Android::audioThreadFunc(void *arg) {
-	JNI::attachThread();
-
-	OSystem_Android *system = (OSystem_Android *)arg;
-	Audio::MixerImpl *mixer = system->_mixer;
-
-	uint buf_size = system->_audio_buffer_size;
-
-	JNIEnv *env = JNI::getEnv();
-
-	jbyteArray bufa = env->NewByteArray(buf_size);
-
-	bool paused = true;
-
-	int offset, left, written, i;
-
-	struct timespec tv_delay;
-	tv_delay.tv_sec = 0;
-	tv_delay.tv_nsec = 20 * 1000 * 1000;
-
-	uint msecs_full = buf_size * 1000 / (mixer->getOutputRate() * 2 * 2);
-
-	struct timespec tv_full;
-	tv_full.tv_sec = 0;
-	tv_full.tv_nsec = msecs_full * 1000 * 1000;
-
-	uint silence_count = 33;
-
-	while (!system->_audio_thread_exit) {
-		if (JNI::pause) {
-			JNI::setAudioStop();
-
-			paused = true;
-			silence_count = 33;
-
-			LOGD("audio thread going to sleep");
-			sem_wait(&JNI::pause_sem);
-			LOGD("audio thread woke up");
-		}
-
-		byte *buf = (byte *)env->GetPrimitiveArrayCritical(bufa, 0);
-		assert(buf);
-
-		int samples = mixer->mixCallback(buf, buf_size);
-
-		bool silence = samples < 1;
-
-		// looks stupid, and it is, but currently there's no way to detect
-		// silence-only buffers from the mixer
-		if (!silence) {
-			silence = true;
-
-			for (i = 0; i < samples; i += 2)
-				// SID streams constant crap
-				if (READ_UINT16(buf + i) > 32) {
-					silence = false;
-					break;
-				}
-		}
-
-		env->ReleasePrimitiveArrayCritical(bufa, buf, 0);
-
-		if (silence) {
-			if (!paused)
-				silence_count++;
-
-			// only pause after a while to prevent toggle mania
-			if (silence_count > 32) {
-				if (!paused) {
-					LOGD("AudioTrack pause");
-
-					JNI::setAudioPause();
-					paused = true;
-				}
-
-				nanosleep(&tv_full, 0);
-
-				continue;
-			}
-		}
-
-		if (paused) {
-			LOGD("AudioTrack play");
-
-			JNI::setAudioPlay();
-			paused = false;
-
-			silence_count = 0;
-		}
-
-		offset = 0;
-		left = buf_size;
-		written = 0;
-
-		while (left > 0) {
-			written = JNI::writeAudio(env, bufa, offset, left);
-
-			if (written < 0) {
-				LOGE("AudioTrack error: %d", written);
-				break;
-			}
-
-			// buffer full
-			if (written < left)
-				nanosleep(&tv_delay, 0);
-
-			offset += written;
-			left -= written;
-		}
-
-		if (written < 0)
-			break;
-
-		// prepare the next buffer, and run into the blocking AudioTrack.write
-	}
-
-	JNI::setAudioStop();
-
-	env->DeleteLocalRef(bufa);
-
-	JNI::detachThread();
-
-	return 0;
-}
-
 //
 // When launching ScummVM (from ScummVMActivity) order of business is as follows:
 // 1. scummvm_main() (base/main.cpp)
@@ -561,17 +424,11 @@ void OSystem_Android::initBackend() {
 
 	gettimeofday(&_startTime, 0);
 
-	// The division by four happens because the Mixer stores the size in frame units
-	// instead of bytes; this means that, since we have audio in stereo (2 channels)
-	// with a word size of 16 bit (2 bytes), we have to divide the effective size by 4.
-	_mixer = new Audio::MixerImpl(_audio_sample_rate, true, _audio_buffer_size / 4);
-	_mixer->setReady(true);
-
 	_timer_thread_exit = false;
 	pthread_create(&_timer_thread, 0, timerThreadFunc, this);
 
-	_audio_thread_exit = false;
-	pthread_create(&_audio_thread, 0, audioThreadFunc, this);
+	_mixerManager = AndroidMixerManager::make();
+	_mixerManager->init();
 
 	JNI::DPIValues dpi;
 	JNI::getDPI(dpi);
@@ -919,13 +776,15 @@ Common::MutexInternal *OSystem_Android::createMutex() {
 void OSystem_Android::quit() {
 	ENTER();
 
-	_audio_thread_exit = true;
+	AndroidMixerManager *mixerManager = dynamic_cast<AndroidMixerManager *>(_mixerManager);
+
+	mixerManager->signalQuit();
 	_timer_thread_exit = true;
 
 	JNI::wakeupForQuit();
 	JNI::setReadyForEvents(false);
 
-	pthread_join(_audio_thread, 0);
+	mixerManager->quit();
 	pthread_join(_timer_thread, 0);
 }
 
@@ -933,11 +792,6 @@ void OSystem_Android::setWindowCaption(const Common::U32String &caption) {
 	JNI::setWindowCaption(caption);
 }
 
-Audio::Mixer *OSystem_Android::getMixer() {
-	assert(_mixer);
-	return _mixer;
-}
-
 void OSystem_Android::getTimeAndDate(TimeDate &td, bool skipRecord) const {
 	struct tm tm;
 	const time_t curTime = time(0);
diff --git a/backends/platform/android/android.h b/backends/platform/android/android.h
index 41bd3f072d6..86e62ee9bb5 100644
--- a/backends/platform/android/android.h
+++ b/backends/platform/android/android.h
@@ -22,12 +22,13 @@
 #ifndef _ANDROID_H_
 #define _ANDROID_H_
 
+#include <android/log.h>
+
 #include "backends/platform/android/portdefs.h"
 #include "common/fs.h"
 #include "common/archive.h"
 #include "common/mutex.h"
 #include "common/ustr.h"
-#include "audio/mixer_intern.h"
 #include "backends/modular-backend.h"
 #include "backends/plugins/posix/posix-provider.h"
 #include "backends/fs/posix/posix-fs-factory.h"
@@ -38,8 +39,6 @@
 
 #include <pthread.h>
 
-#include <android/log.h>
-
 // toggles start
 //#define ANDROID_DEBUG_ENTER
 //#define ANDROID_DEBUG_GL
@@ -97,7 +96,7 @@ extern void checkGlError(const char *expr, const char *file, int line);
 
 void *androidGLgetProcAddress(const char *name);
 
-class OSystem_Android : public ModularGraphicsBackend, Common::EventSource {
+class OSystem_Android : public ModularGraphicsBackend, public ModularMixerBackend, Common::EventSource {
 private:
 	static const int kQueuedInputEventDelay = 50;
 
@@ -129,10 +128,6 @@ private:
 		}
 	};
 
-	// passed from the dark side
-	int _audio_sample_rate;
-	int _audio_buffer_size;
-
 	int _screen_changeid;
 
 	pthread_t _main_thread;
@@ -140,12 +135,8 @@ private:
 	bool _timer_thread_exit;
 	pthread_t _timer_thread;
 
-	bool _audio_thread_exit;
-	pthread_t _audio_thread;
-
 	bool _virtkeybd_on;
 
-	Audio::MixerImpl *_mixer;
 	timeval _startTime;
 
 	PauseToken _pauseToken;
@@ -185,7 +176,7 @@ private:
 #endif
 
 	static void *timerThreadFunc(void *arg);
-	static void *audioThreadFunc(void *arg);
+	void initAudio();
 	Common::String getSystemProperty(const char *name) const;
 
 	Common::WriteStream *createLogFileForAppending();
@@ -211,7 +202,7 @@ public:
 		SHOW_ON_SCREEN_ALL = 0xffffffff,
 	};
 
-	OSystem_Android(int audio_sample_rate, int audio_buffer_size);
+	OSystem_Android();
 	virtual ~OSystem_Android();
 
 	void initBackend() override;
@@ -259,7 +250,6 @@ public:
 
 	void setWindowCaption(const Common::U32String &caption) override;
 
-	Audio::Mixer *getMixer() override;
 	void getTimeAndDate(TimeDate &td, bool skipRecord = false) const override;
 	void logMessage(LogMessageType::Type type, const char *message) override;
 	void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0) override;
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index 9647ea74dc6..cd004b24436 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -62,7 +62,6 @@ pthread_key_t JNI::_env_tls;
 
 JavaVM *JNI::_vm = 0;
 jobject JNI::_jobj = 0;
-jobject JNI::_jobj_audio_track = 0;
 jobject JNI::_jobj_egl = 0;
 jobject JNI::_jobj_egl_display = 0;
 jobject JNI::_jobj_egl_surface = 0;
@@ -115,17 +114,11 @@ jmethodID JNI::_MID_importBackup = 0;
 
 jmethodID JNI::_MID_EGL10_eglSwapBuffers = 0;
 
-jmethodID JNI::_MID_AudioTrack_flush = 0;
-jmethodID JNI::_MID_AudioTrack_pause = 0;
-jmethodID JNI::_MID_AudioTrack_play = 0;
-jmethodID JNI::_MID_AudioTrack_stop = 0;
-jmethodID JNI::_MID_AudioTrack_write = 0;
-
 const JNINativeMethod JNI::_natives[] = {
 	{ "create", "(Landroid/content/res/AssetManager;"
 				"Ljavax/microedition/khronos/egl/EGL10;"
 				"Ljavax/microedition/khronos/egl/EGLDisplay;"
-				"Landroid/media/AudioTrack;IIZ)V",
+				"Z)V",
 		(void *)JNI::create },
 	{ "destroy", "()V",
 		(void *)JNI::destroy },
@@ -145,6 +138,10 @@ const JNINativeMethod JNI::_natives[] = {
 		(void *)JNI::setPause },
 	{ "systemInsetsUpdated", "([I[I[I)V",
 		(void *)JNI::systemInsetsUpdated },
+	{ "setDefaultAudioValues", "(II)V",
+		(void *)JNI::setDefaultAudioValues },
+	{ "notifyAudioDisconnect", "()V",
+		(void *)JNI::notifyAudioDisconnect },
 	{ "getNativeVersionInfo", "()Ljava/lang/String;",
 		(void *)JNI::getNativeVersionInfo }
 };
@@ -683,59 +680,10 @@ int JNI::fetchEGLVersion() {
 	return _egl_version;
 }
 
-void JNI::setAudioPause() {
-	JNIEnv *env = JNI::getEnv();
-
-	env->CallVoidMethod(_jobj_audio_track, _MID_AudioTrack_flush);
-
-	if (env->ExceptionCheck()) {
-		LOGE("Error flushing AudioTrack");
-
-		env->ExceptionDescribe();
-		env->ExceptionClear();
-	}
-
-	env->CallVoidMethod(_jobj_audio_track, _MID_AudioTrack_pause);
-
-	if (env->ExceptionCheck()) {
-		LOGE("Error setting AudioTrack: pause");
-
-		env->ExceptionDescribe();
-		env->ExceptionClear();
-	}
-}
-
-void JNI::setAudioPlay() {
-	JNIEnv *env = JNI::getEnv();
-
-	env->CallVoidMethod(_jobj_audio_track, _MID_AudioTrack_play);
-
-	if (env->ExceptionCheck()) {
-		LOGE("Error setting AudioTrack: play");
-
-		env->ExceptionDescribe();
-		env->ExceptionClear();
-	}
-}
-
-void JNI::setAudioStop() {
-	JNIEnv *env = JNI::getEnv();
-
-	env->CallVoidMethod(_jobj_audio_track, _MID_AudioTrack_stop);
-
-	if (env->ExceptionCheck()) {
-		LOGE("Error setting AudioTrack: stop");
-
-		env->ExceptionDescribe();
-		env->ExceptionClear();
-	}
-}
-
 // natives for the dark side
 
 void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 				jobject egl, jobject egl_display,
-				jobject at, jint audio_sample_rate, jint audio_buffer_size,
 				jboolean assets_updated_) {
 	LOGI("Native version: %s", gScummVMFullVersion);
 
@@ -800,18 +748,6 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 				"(Ljavax/microedition/khronos/egl/EGLDisplay;"
 				"Ljavax/microedition/khronos/egl/EGLSurface;)Z");
 
-	_jobj_audio_track = env->NewGlobalRef(at);
-
-	env->DeleteLocalRef(cls);
-
-	cls = env->GetObjectClass(_jobj_audio_track);
-
-	FIND_METHOD(AudioTrack_, flush, "()V");
-	FIND_METHOD(AudioTrack_, pause, "()V");
-	FIND_METHOD(AudioTrack_, play, "()V");
-	FIND_METHOD(AudioTrack_, stop, "()V");
-	FIND_METHOD(AudioTrack_, write, "([BII)I");
-
 	env->DeleteLocalRef(cls);
 #undef FIND_METHOD
 
@@ -831,7 +767,7 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
 	_asset_archive = new AndroidAssetArchive(asset_manager);
 	assert(_asset_archive);
 
-	_system = new OSystem_Android(audio_sample_rate, audio_buffer_size);
+	_system = new OSystem_Android();
 	assert(_system);
 
 	g_system = _system;
@@ -856,7 +792,6 @@ void JNI::destroy(JNIEnv *env, jobject self) {
 
 	JNI::getEnv()->DeleteGlobalRef(_jobj_egl_display);
 	JNI::getEnv()->DeleteGlobalRef(_jobj_egl);
-	JNI::getEnv()->DeleteGlobalRef(_jobj_audio_track);
 	JNI::getEnv()->DeleteGlobalRef(_jobj);
 }
 
@@ -994,6 +929,8 @@ void JNI::systemInsetsUpdated(JNIEnv *env, jobject self, jintArray gestureInsets
 	env->GetIntArrayRegion(cutoutInsets, 0, ARRAYSIZE(cutout_insets), cutout_insets);
 }
 
+// JNI::setDefaultAudioValues and JNI::notifyAudioDisconnect are in android-mixer.cpp
+
 jstring JNI::getNativeVersionInfo(JNIEnv *env, jobject self) {
 	return convertToJString(env, Common::U32String(gScummVMVersion));
 }
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index 7debf08874d..2c8d6aab412 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -112,13 +112,6 @@ public:
 		return fetchEGLVersion();
 	}
 
-	static void setAudioPause();
-	static void setAudioPlay();
-	static void setAudioStop();
-
-	static inline int writeAudio(JNIEnv *env, jbyteArray &data, int offset,
-									int size);
-
 	static Common::Array<Common::String> getAllStorageLocations();
 
 	static jobject getNewSAFTree(bool writable, const Common::String &initURI, const Common::String &prompt);
@@ -134,7 +127,6 @@ private:
 	static JavaVM *_vm;
 	// back pointer to (java) peer instance
 	static jobject _jobj;
-	static jobject _jobj_audio_track;
 	static jobject _jobj_egl;
 	static jobject _jobj_egl_display;
 	static jobject _jobj_egl_surface;
@@ -176,12 +168,6 @@ private:
 
 	static jmethodID _MID_EGL10_eglSwapBuffers;
 
-	static jmethodID _MID_AudioTrack_flush;
-	static jmethodID _MID_AudioTrack_pause;
-	static jmethodID _MID_AudioTrack_play;
-	static jmethodID _MID_AudioTrack_stop;
-	static jmethodID _MID_AudioTrack_write;
-
 	static const JNINativeMethod _natives[];
 
 	static void throwByName(JNIEnv *env, const char *name, const char *msg);
@@ -190,8 +176,6 @@ private:
 	// natives for the dark side
 	static void create(JNIEnv *env, jobject self, jobject asset_manager,
 						jobject egl, jobject egl_display,
-						jobject at, jint audio_sample_rate,
-						jint audio_buffer_size,
 						jboolean assets_updated_);
 	static void destroy(JNIEnv *env, jobject self);
 
@@ -207,6 +191,9 @@ private:
 
 	static void systemInsetsUpdated(JNIEnv *env, jobject self, jintArray gestureInsets, jintArray systemInsets, jintArray cutoutInsets);
 
+	static void setDefaultAudioValues(JNIEnv *env, jclass clazz, jint sampleRate, jint framesPerBurst);
+	static void notifyAudioDisconnect(JNIEnv *env, jclass clazz);
+
 	static jstring getNativeVersionInfo(JNIEnv *env, jobject self);
 	static jstring convertToJString(JNIEnv *env, const Common::U32String &str);
 	static Common::U32String convertFromJString(JNIEnv *env, const jstring &jstr);
@@ -226,9 +213,4 @@ inline bool JNI::swapBuffers() {
 									_jobj_egl_display, _jobj_egl_surface);
 }
 
-inline int JNI::writeAudio(JNIEnv *env, jbyteArray &data, int offset, int size) {
-	return env->CallIntMethod(_jobj_audio_track, _MID_AudioTrack_write, data,
-								offset, size);
-}
-
 #endif
diff --git a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
index 93067e82067..9fc9ea268e6 100644
--- a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
+++ b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
@@ -1,8 +1,10 @@
 package org.scummvm.scummvm;
 
 import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.content.res.Resources;
@@ -261,86 +263,6 @@ class CompatHelpers {
 		}
 	}
 
-	static class AudioTrackCompat {
-		public static class AudioTrackCompatReturn {
-			public AudioTrack audioTrack;
-			public int bufferSize;
-		}
-
-		public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
-			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
-				return AudioTrackCompatM.make(sample_rate, buffer_size);
-			} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
-				return AudioTrackCompatLollipop.make(sample_rate, buffer_size);
-			} else {
-				return AudioTrackCompatOld.make(sample_rate, buffer_size);
-			}
-		}
-
-		/**
-		 * Support for Android KitKat or lower
-		 */
-		@SuppressWarnings("deprecation")
-		private static class AudioTrackCompatOld {
-			public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
-				AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
-				ret.audioTrack = new AudioTrack(
-					AudioManager.STREAM_MUSIC,
-					sample_rate,
-					AudioFormat.CHANNEL_OUT_STEREO,
-					AudioFormat.ENCODING_PCM_16BIT,
-					buffer_size,
-					AudioTrack.MODE_STREAM);
-				ret.bufferSize = buffer_size;
-				return ret;
-			}
-		}
-
-		@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
-		private static class AudioTrackCompatLollipop {
-			public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
-				AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
-				ret.audioTrack = new AudioTrack(
-					new AudioAttributes.Builder()
-						.setUsage(AudioAttributes.USAGE_GAME)
-						.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
-						.build(),
-					new AudioFormat.Builder()
-						.setSampleRate(sample_rate)
-						.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
-						.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(),
-					buffer_size,
-					AudioTrack.MODE_STREAM,
-					AudioManager.AUDIO_SESSION_ID_GENERATE);
-				ret.bufferSize = buffer_size;
-				return ret;
-			}
-		}
-
-		@RequiresApi(android.os.Build.VERSION_CODES.M)
-		private static class AudioTrackCompatM {
-			public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
-				AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
-				ret.audioTrack = new AudioTrack(
-					new AudioAttributes.Builder()
-						.setUsage(AudioAttributes.USAGE_GAME)
-						.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
-						.build(),
-					new AudioFormat.Builder()
-						.setSampleRate(sample_rate)
-						.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
-						.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(),
-					buffer_size,
-					AudioTrack.MODE_STREAM,
-					AudioManager.AUDIO_SESSION_ID_GENERATE);
-				// Keep track of the actual obtained audio buffer size, if supported.
-				// We just requested 16 bit PCM stereo pcm so there are 4 bytes per frame.
-				ret.bufferSize = ret.audioTrack.getBufferSizeInFrames() * 4;
-				return ret;
-			}
-		}
-	}
-
 	static class AccessibilityEventConstructor {
 		public static AccessibilityEvent make(int eventType) {
 			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
@@ -563,4 +485,52 @@ class CompatHelpers {
 			}
 		}
 	}
+
+	static class ReceiverCompat {
+		public static Intent registerReceiver(Context context, BroadcastReceiver receiver, IntentFilter filter) {
+			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+				return ReceiverCompat.ReceiverCompatTiramisu.registerReceiver(context, receiver, filter);
+			} else {
+				return ReceiverCompat.ReceiverCompatOld.registerReceiver(context, receiver, filter);
+			}
+		}
+
+		@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU)
+		private static class ReceiverCompatTiramisu {
+			public static Intent registerReceiver(Context context, BroadcastReceiver receiver, IntentFilter filter) {
+				return context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
+			}
+		}
+
+		private static class ReceiverCompatOld {
+			@SuppressLint("UnspecifiedRegisterReceiverFlag")
+			public static Intent registerReceiver(Context context, BroadcastReceiver receiver, IntentFilter filter) {
+				return context.registerReceiver(receiver, filter);
+			}
+		}
+	}
+
+	static class IntentCompat {
+		public static <T extends android.os.Parcelable> T getParcelableExtra(Intent i, String extra, Class<T> clazz) {
+			if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+				return IntentCompat.IntentCompatTiramisu.getParcelableExtra(i, extra, clazz);
+			} else {
+				return IntentCompat.IntentCompatOld.getParcelableExtra(i, extra, clazz);
+			}
+		}
+
+		@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU)
+		private static class IntentCompatTiramisu {
+			public static <T extends android.os.Parcelable> T getParcelableExtra(Intent i, String extra, Class<T> clazz) {
+				return i.getParcelableExtra(extra, clazz);
+			}
+		}
+
+		@SuppressWarnings("deprecation")
+		private static class IntentCompatOld {
+			public static <T extends android.os.Parcelable> T getParcelableExtra(Intent i, String extra, Class<T> clazz) {
+				return i.getParcelableExtra(extra);
+			}
+		}
+	}
 }
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index 4cbb498a9c2..c46dbcde718 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -2,9 +2,6 @@ package org.scummvm.scummvm;
 
 import android.content.res.AssetManager;
 import android.graphics.PixelFormat;
-import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.media.AudioTrack;
 import android.util.Log;
 import android.view.SurfaceHolder;
 
@@ -39,9 +36,6 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
 
 	private SurfaceHolder _surface_holder;
 	private int bitsPerPixel;
-	private AudioTrack _audio_track;
-	private int _sample_rate = 0;
-	private int _buffer_size = 0;
 
 	private boolean _assetsUpdated;
 	private String[] _args;
@@ -49,9 +43,6 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
 	private native void create(AssetManager asset_manager,
 	                           EGL10 egl,
 	                           EGLDisplay egl_display,
-	                           AudioTrack audio_track,
-	                           int sample_rate,
-	                           int buffer_size,
 	                           boolean assetsUpdated);
 	private native void destroy();
 	private native void setSurface(int width, int height, int bpp);
@@ -68,6 +59,9 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
 
 	final public native void syncVirtkeyboardState(boolean newState);
 
+	public static native void setDefaultAudioValues(int sampleRate, int framesPerBurst);
+	public static native void notifyAudioDisconnect();
+
 	final public native String getNativeVersionInfo();
 
 	// CompatHelpers.WindowInsets.SystemInsetsListener interface
@@ -174,17 +168,14 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
 					_sem_surface.wait();
 			}
 
-			initAudio();
 			initEGL();
 		} catch (Exception e) {
 			deinitEGL();
-			deinitAudio();
 
 			throw new RuntimeException("Error preparing the ScummVM thread", e);
 		}
 
 		create(_asset_manager, _egl, _egl_display,
-				_audio_track, _sample_rate, _buffer_size,
 				_assetsUpdated);
 
 		int res = main(_args);
@@ -192,7 +183,6 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
 		destroy();
 
 		deinitEGL();
-		deinitAudio();
 
 		// Don't exit force-ably here!
 		if (_svm_destroyed_callback != null) {
@@ -306,45 +296,6 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
 		_egl = null;
 	}
 
-	private void initAudio() throws Exception {
-		_sample_rate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
-		_buffer_size = AudioTrack.getMinBufferSize(_sample_rate,
-		                                           AudioFormat.CHANNEL_OUT_STEREO,
-		                                           AudioFormat.ENCODING_PCM_16BIT);
-
-		// ~50ms
-		int buffer_size_want = (_sample_rate * 2 * 2 / 20) & ~1023;
-
-		if (_buffer_size < buffer_size_want) {
-			Log.w(LOG_TAG, String.format(Locale.ROOT,
-				"adjusting audio buffer size (was: %d)", _buffer_size));
-
-			_buffer_size = buffer_size_want;
-		}
-
-		Log.i(LOG_TAG, String.format(Locale.ROOT, "Using %d bytes buffer for %dHz audio",
-										_buffer_size, _sample_rate));
-
-		CompatHelpers.AudioTrackCompat.AudioTrackCompatReturn audioTrackRet =
-			CompatHelpers.AudioTrackCompat.make(_sample_rate, _buffer_size);
-		_audio_track = audioTrackRet.audioTrack;
-		_buffer_size = audioTrackRet.bufferSize;
-
-		if (_audio_track.getState() != AudioTrack.STATE_INITIALIZED)
-			throw new Exception(
-				String.format(Locale.ROOT, "Error initializing AudioTrack: %d",
-								_audio_track.getState()));
-	}
-
-	private void deinitAudio() {
-		if (_audio_track != null)
-			_audio_track.release();
-
-		_audio_track = null;
-		_buffer_size = 0;
-		_sample_rate = 0;
-	}
-
 	private static final int[] s_eglAttribs = {
 		EGL10.EGL_CONFIG_ID,
 		EGL10.EGL_BUFFER_SIZE,
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 41725a0a30c..78306e0eebd 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -4,17 +4,25 @@ import android.Manifest;
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
 import android.content.ClipboardManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.res.AssetManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.media.AudioFormat;
 import android.media.AudioManager;
+import android.media.AudioTrack;
 import android.net.ConnectivityManager;
 import android.net.Uri;
 import android.os.Build;
@@ -130,6 +138,8 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 	private InputMethodManager _inputManager = null;
 
+	private PluginBroadcastReceiver _pluginBroadcastReceiver = null;
+
 	// Set to true in onDestroy
 	// This avoids that when C++ terminates we call finish() a second time
 	// This second finish causes termination when we are launched again
@@ -1010,7 +1020,9 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		//_main_surface.captureMouse(true, true);
 		//_main_surface.showSystemMouseCursor(false);
 
+		updateAudioValues();
 		setVolumeControlStream(AudioManager.STREAM_MUSIC);
+		_pluginBroadcastReceiver = new PluginBroadcastReceiver();
 
 		// TODO needed?
 		takeKeyEvents(true);
@@ -1206,6 +1218,8 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		//_main_surface.showSystemMouseCursor(false);
 		//Log.d(ScummVM.LOG_TAG, "onResume - captureMouse(true)");
 		_main_surface.captureMouse(true);
+
+		_pluginBroadcastReceiver.register(this);
 	}
 
 	@Override
@@ -1216,6 +1230,8 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 
 		super.onPause();
 
+		_pluginBroadcastReceiver.unregister(this);
+
 		if (_scummvm != null)
 			_scummvm.setPause(true);
 		//_main_surface.showSystemMouseCursor(true);
@@ -1462,6 +1478,153 @@ public class ScummVMActivity extends Activity implements OnKeyboardVisibilityLis
 		return getWindowManager().getDefaultDisplay().getPixelFormat();
 	}
 
+	// region Audio/Oboe helpers
+
+	private void updateAudioValues() {
+		// These values are useless on Android Oreo and above as AAudio doesn't use them
+		/*
+		PackageManager pm = getPackageManager();
+		boolean hasLL = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
+		*/
+
+		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+			int audioTrackSampleRate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
+			int audioTrackFramesPerBurst = AudioTrack.getMinBufferSize(audioTrackSampleRate,
+				AudioFormat.CHANNEL_OUT_STEREO,
+				AudioFormat.ENCODING_PCM_16BIT);
+			audioTrackFramesPerBurst /= 2 * 2; // Convert Stereo 16-bits to frames
+			audioTrackFramesPerBurst /= 4; // AudioTrack tends to buffer a lot
+
+			Log.d(ScummVM.LOG_TAG,  "updateAudioValues:" +
+				" at=" + Integer.toString(audioTrackSampleRate) + "/" + Integer.toString(audioTrackFramesPerBurst));
+
+			ScummVM.setDefaultAudioValues(audioTrackSampleRate, audioTrackFramesPerBurst);
+			return;
+		}
+
+		AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+		String text = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+		int audioManagerSampleRate = Integer.parseInt(text);
+		text = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+		int audioManagerFramesPerBurst = Integer.parseInt(text);
+
+		Log.d(ScummVM.LOG_TAG,  "updateAudioValues:" +
+			" am=" + Integer.toString(audioManagerSampleRate) + "/" + Integer.toString(audioManagerFramesPerBurst));
+
+		ScummVM.setDefaultAudioValues(audioManagerSampleRate, audioManagerFramesPerBurst);
+	}
+
+	/**
+	 * This BroadcastReceiver works around an AAudio/oboe bug
+	 * cf. <a href="https://github.com/google/oboe/wiki/TechNote_Disconnect">Oboe doc</a>
+	 */
+	private static class PluginBroadcastReceiver extends BroadcastReceiver {
+		private static final String ACTION_HEADSET_PLUG	=
+			(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ?
+				AudioManager.ACTION_HEADSET_PLUG :
+				Intent.ACTION_HEADSET_PLUG;
+
+		private int lastStatus = -1;
+
+		private IntentFilter getIntentFilter() {
+			IntentFilter filter = new IntentFilter(ACTION_HEADSET_PLUG);
+			filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+			filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+			return filter;
+		}
+
+		void register(Context ctx) {
+			if (Build.VERSION_CODES.P <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+				CompatHelpers.ReceiverCompat.registerReceiver(ctx, this, getIntentFilter());
+			}
+		}
+
+		void unregister(Context ctx) {
+			if (Build.VERSION_CODES.P <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+				ctx.unregisterReceiver(this);
+			}
+		}
+
+		@Override
+		public void onReceive(Context context, @NonNull Intent intent) {
+			// Close the stream if it was not disconnected.
+			String action = intent.getAction();
+			if (ACTION_HEADSET_PLUG.equals(action)) {
+				boolean micro = intent.getIntExtra("microphone", -1) == 1;
+				boolean state = intent.getIntExtra("state", -1) == 1;
+				int newStatus = (micro ? 1 : 0) + (state ? 2 : 0);
+
+				Log.i(ScummVM.LOG_TAG, action +
+					" micro=" + Boolean.toString(micro) +
+					" state=" + Boolean.toString(state) +
+					" status=" + Integer.toString(newStatus) +
+					" lastStatus=" + Integer.toString(lastStatus) +
+					" diff=" + Integer.toString(lastStatus ^ newStatus));
+
+				if (isInitialStickyBroadcast()) {
+					if (lastStatus == -1) {
+						lastStatus = newStatus;
+						return;
+					}
+				}
+
+				if (((lastStatus ^ newStatus) & 2) == 0) {
+					// We are only interested in a state change
+					return;
+				}
+
+				lastStatus = newStatus;
+				ScummVM.notifyAudioDisconnect();
+			}
+			else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action) ||
+				UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
+				UsbDevice device = CompatHelpers.IntentCompat.getParcelableExtra(intent, UsbManager.EXTRA_DEVICE, UsbDevice.class);
+				if (device == null) {
+					return;
+				}
+				final boolean hasAudioPlayback =
+					containsAudioStreamingInterface(device, UsbConstants.USB_DIR_OUT);
+				final boolean hasAudioCapture =
+					containsAudioStreamingInterface(device, UsbConstants.USB_DIR_IN);
+				Log.w(ScummVM.LOG_TAG, action + " device=" + device.toString() + " playback=" + Boolean.toString(hasAudioPlayback) + " capture=" + Boolean.toString(hasAudioCapture));
+				if (!hasAudioPlayback) {
+					// We are only interested in playback sinks
+					return;
+				}
+				ScummVM.notifyAudioDisconnect();
+			}
+		}
+
+		private static final int AUDIO_STREAMING_SUB_CLASS = 2;
+
+		/**
+		 * Figure out if an UsbDevice contains audio input/output streaming interface or not.
+		 *
+		 * @param device the given UsbDevice
+		 * @param direction the direction of the audio streaming interface
+		 * @return true if the UsbDevice contains the audio input/output streaming interface.
+		 */
+		private boolean containsAudioStreamingInterface(UsbDevice device, int direction) {
+			final int interfaceCount = device.getInterfaceCount();
+			for (int i = 0; i < interfaceCount; ++i) {
+				UsbInterface usbInterface = device.getInterface(i);
+				if (usbInterface.getInterfaceClass() != UsbConstants.USB_CLASS_AUDIO
+					&& usbInterface.getInterfaceSubclass() != AUDIO_STREAMING_SUB_CLASS) {
+					continue;
+				}
+				final int endpointCount = usbInterface.getEndpointCount();
+				for (int j = 0; j < endpointCount; ++j) {
+					if (usbInterface.getEndpoint(j).getDirection() == direction) {
+						return true;
+					}
+				}
+			}
+			return false;
+		}
+	}
+
+	// endregion
+
 	// region Configuration migration and internal folder init
 	// -------------------------------------------------------------------------------------------
 
diff --git a/configure b/configure
index 33e8fda1622..ab956de19f1 100755
--- a/configure
+++ b/configure
@@ -7729,7 +7729,7 @@ case $_host_os in
 			esac
 		done
 
-		LIBS="-Wl,-Bstatic $static_libs -Wl,-Bdynamic $system_libs -llog -landroid -ljnigraphics -lEGL"
+		LIBS="-Wl,-Bstatic $static_libs -Wl,-Bdynamic $system_libs -llog -landroid -loboe -ljnigraphics -lEGL"
 		;;
 	ds)
 		# Moved -Wl,--gc-sections here to avoid it interfering with the library checks




More information about the Scummvm-git-logs mailing list