mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-12 09:09:29 +01:00
An Android 11 TV played nothing over a Bluetooth headset since the switch to Oboe, while its speakers worked. Testing on it showed AAudio with MMAP is silent there and AAudio without MMAP works, as does OpenSL ES (also when the headset is connected mid-game), whatever the stream usage. MMAP should never be used for a Bluetooth route, so it's a firmware bug. Audio settings get an Audio API choice and a Low-latency audio (MMAP) checkbox, with hints, so other devices with the same kind of bug can be worked around. The API defaults to OpenSL ES on TVs and before Android 8.1, where the choice isn't offered, and AAudio elsewhere. Changing them reopens the streams right away. System Information shows the output stream that got opened (API, MMAP or not, rate, buffers, device) and any audio error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
213 lines
7.3 KiB
C++
213 lines
7.3 KiB
C++
// Audio output and microphone input through Oboe, which uses AAudio where it's
|
|
// available (Android 8.1+) and falls back to OpenSL ES on older devices.
|
|
|
|
#include <mutex>
|
|
|
|
#include <oboe/Oboe.h>
|
|
|
|
#include "Common/Log.h"
|
|
#include "Common/System/System.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/ConfigValues.h"
|
|
#include "android/jni/OboeContext.h"
|
|
|
|
// Owns the streams. Held by shared_ptr since after a disconnect, Oboe calls the error
|
|
// callback on a thread of its own that can outlive the OboeContext.
|
|
class OboeContext::Streams : public oboe::AudioStreamDataCallback, public oboe::AudioStreamErrorCallback, public std::enable_shared_from_this<OboeContext::Streams> {
|
|
public:
|
|
Streams(AndroidAudioCallback cb, int framesPerBuffer, int sampleRate)
|
|
: audioCallback_(cb), framesPerBuffer_(framesPerBuffer), sampleRate_(sampleRate) {
|
|
// The setting isn't offered before Android 8.1, where AAudio isn't stable.
|
|
if (g_Config.iAndroidAudioApi == (int)AndroidAudioApi::AAUDIO && oboe::AudioStreamBuilder::isAAudioRecommended()) {
|
|
audioApi_ = oboe::AudioApi::AAudio;
|
|
}
|
|
// Global in Oboe, and only read when an AAudio stream is opened.
|
|
oboe::OboeExtensions::setMMapEnabled(g_Config.bAndroidAudioMMap);
|
|
}
|
|
|
|
bool StartOutput() {
|
|
std::lock_guard<std::mutex> guard(mutex_);
|
|
return OpenOutput();
|
|
}
|
|
|
|
bool StartInput(int sampleRate) {
|
|
std::lock_guard<std::mutex> guard(mutex_);
|
|
CloseStream(input_);
|
|
// The PSP mic runs at 44100, 22050 or 11025 Hz. Oboe resamples from whatever the device has.
|
|
inputSampleRate_ = sampleRate > 0 ? sampleRate : 44100;
|
|
return OpenInput();
|
|
}
|
|
|
|
void StopInput() {
|
|
std::lock_guard<std::mutex> guard(mutex_);
|
|
CloseStream(input_);
|
|
inputSampleRate_ = 0;
|
|
}
|
|
|
|
void Shutdown() {
|
|
std::lock_guard<std::mutex> guard(mutex_);
|
|
shutdown_ = true;
|
|
CloseStream(output_);
|
|
CloseStream(input_);
|
|
}
|
|
|
|
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *stream, void *audioData, int32_t numFrames) override {
|
|
if (stream->getDirection() == oboe::Direction::Output) {
|
|
audioCallback_((short *)audioData, numFrames, stream->getSampleRate(), nullptr);
|
|
} else {
|
|
// This is the audio thread, so just buffer it. The CPU thread picks it up.
|
|
AndroidAudio_Recording_Push((const int16_t *)audioData, numFrames);
|
|
}
|
|
return oboe::DataCallbackResult::Continue;
|
|
}
|
|
|
|
// Oboe has already closed the stream by now. A disconnect (headphones unplugged, a Bluetooth
|
|
// device going away) means the route changed, so open a new stream on the new default device.
|
|
void onErrorAfterClose(oboe::AudioStream *stream, oboe::Result error) override {
|
|
std::lock_guard<std::mutex> guard(mutex_);
|
|
if (shutdown_) {
|
|
return;
|
|
}
|
|
const bool isOutput = stream == output_.get();
|
|
const bool isInput = stream == input_.get();
|
|
if (!isOutput && !isInput) {
|
|
return;
|
|
}
|
|
WARN_LOG(Log::Audio, "Oboe: %s stream closed with error: %s", isOutput ? "output" : "input", oboe::convertToText(error));
|
|
if (isOutput) {
|
|
output_.reset();
|
|
} else {
|
|
input_.reset();
|
|
}
|
|
if (error != oboe::Result::ErrorDisconnected) {
|
|
OboeContext::SetErrorString(std::string("Stream error: ") + oboe::convertToText(error));
|
|
return;
|
|
}
|
|
if (isOutput) {
|
|
OpenOutput();
|
|
} else if (inputSampleRate_) {
|
|
OpenInput();
|
|
}
|
|
}
|
|
|
|
private:
|
|
// The ones below are called with mutex_ held.
|
|
bool OpenOutput() {
|
|
oboe::AudioStreamBuilder builder;
|
|
builder.setDirection(oboe::Direction::Output)
|
|
->setAudioApi(audioApi_)
|
|
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
|
->setSharingMode(oboe::SharingMode::Shared)
|
|
->setUsage(oboe::Usage::Game)
|
|
->setFormat(oboe::AudioFormat::I16)
|
|
->setFormatConversionAllowed(true)
|
|
->setChannelCount(oboe::ChannelCount::Stereo)
|
|
->setChannelConversionAllowed(true)
|
|
->setSampleRate(sampleRate_)
|
|
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium)
|
|
->setFramesPerDataCallback(framesPerBuffer_)
|
|
->setDataCallback(this)
|
|
->setErrorCallback(shared_from_this());
|
|
oboe::Result result = builder.openStream(output_);
|
|
if (result != oboe::Result::OK) {
|
|
output_.reset();
|
|
return Fail("Failed to open output stream", result);
|
|
}
|
|
// Double buffering, the same latency as the old OpenSL path.
|
|
output_->setBufferSizeInFrames(output_->getFramesPerBurst() * 2);
|
|
result = output_->requestStart();
|
|
if (result != oboe::Result::OK) {
|
|
CloseStream(output_);
|
|
return Fail("Failed to start output stream", result);
|
|
}
|
|
char info[256];
|
|
snprintf(info, sizeof(info), "%s%s, %d Hz, burst %d, buffer %d, %s, device %d",
|
|
oboe::convertToText(output_->getAudioApi()),
|
|
// Only ask AAudio. An OpenSL ES stream has no AAudio stream underneath.
|
|
output_->getAudioApi() == oboe::AudioApi::AAudio && oboe::OboeExtensions::isMMapUsed(output_.get()) ? " (MMAP)" : "",
|
|
output_->getSampleRate(), output_->getFramesPerBurst(), output_->getBufferSizeInFrames(),
|
|
oboe::convertToText(output_->getPerformanceMode()), output_->getDeviceId());
|
|
INFO_LOG(Log::Audio, "Oboe: Output stream open: %s", info);
|
|
OboeContext::SetInfoString(info);
|
|
return true;
|
|
}
|
|
|
|
bool OpenInput() {
|
|
oboe::AudioStreamBuilder builder;
|
|
builder.setDirection(oboe::Direction::Input)
|
|
->setAudioApi(audioApi_)
|
|
->setSharingMode(oboe::SharingMode::Shared)
|
|
->setFormat(oboe::AudioFormat::I16)
|
|
->setFormatConversionAllowed(true)
|
|
->setChannelCount(oboe::ChannelCount::Mono)
|
|
->setChannelConversionAllowed(true)
|
|
->setSampleRate(inputSampleRate_)
|
|
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium)
|
|
->setDataCallback(this)
|
|
->setErrorCallback(shared_from_this());
|
|
oboe::Result result = builder.openStream(input_);
|
|
if (result != oboe::Result::OK) {
|
|
input_.reset();
|
|
return Fail("Failed to open input stream", result);
|
|
}
|
|
result = input_->requestStart();
|
|
if (result != oboe::Result::OK) {
|
|
CloseStream(input_);
|
|
return Fail("Failed to start input stream", result);
|
|
}
|
|
INFO_LOG(Log::Audio, "Oboe: Input stream open: %d Hz", input_->getSampleRate());
|
|
return true;
|
|
}
|
|
|
|
static void CloseStream(std::shared_ptr<oboe::AudioStream> &stream) {
|
|
if (stream) {
|
|
stream->stop();
|
|
stream->close();
|
|
stream.reset();
|
|
}
|
|
}
|
|
|
|
static bool Fail(const char *what, oboe::Result result) {
|
|
ERROR_LOG(Log::Audio, "Oboe: %s: %s", what, oboe::convertToText(result));
|
|
OboeContext::SetErrorString(std::string(what) + ": " + oboe::convertToText(result));
|
|
return false;
|
|
}
|
|
|
|
AndroidAudioCallback audioCallback_;
|
|
int framesPerBuffer_;
|
|
int sampleRate_;
|
|
oboe::AudioApi audioApi_ = oboe::AudioApi::OpenSLES;
|
|
|
|
std::mutex mutex_;
|
|
bool shutdown_ = false;
|
|
std::shared_ptr<oboe::AudioStream> output_;
|
|
std::shared_ptr<oboe::AudioStream> input_;
|
|
int inputSampleRate_ = 0;
|
|
};
|
|
|
|
OboeContext::OboeContext(AndroidAudioCallback cb, int _FramesPerBuffer, int _SampleRate)
|
|
: AudioContext(cb, _FramesPerBuffer, _SampleRate) {
|
|
// Only used by the OpenSL ES fallback, which can't query the device's native values itself.
|
|
oboe::DefaultStreamValues::SampleRate = sampleRate;
|
|
oboe::DefaultStreamValues::FramesPerBurst = framesPerBuffer;
|
|
streams_ = std::make_shared<Streams>(cb, framesPerBuffer, sampleRate);
|
|
}
|
|
|
|
bool OboeContext::Init() {
|
|
return streams_->StartOutput();
|
|
}
|
|
|
|
bool OboeContext::AudioRecord_Start(int sampleRate) {
|
|
return streams_->StartInput(sampleRate);
|
|
}
|
|
|
|
bool OboeContext::AudioRecord_Stop() {
|
|
streams_->StopInput();
|
|
return true;
|
|
}
|
|
|
|
OboeContext::~OboeContext() {
|
|
streams_->Shutdown();
|
|
INFO_LOG(Log::Audio, "Oboe: Shutdown - finished");
|
|
}
|