Pick the Android audio devices, microphone preset and playback usage
Microphone and speaker are chosen separately, so a Bluetooth headset can play in media quality while the phone's own microphone listens, instead of the headset dropping into its call profile. Devices are kept by type and address: Android hands out new ids whenever one reconnects, and a line that lost its device comes back to it when it returns. The microphone preset (voice recognition by default, or the platform's call processing, unprocessed, ...) and whether voices play as media or as a call are Android's own settings. Changing either reopens the open streams in place. Communication mode, and the headset's call link that a Bluetooth microphone needs, are held only while a stream needs them, and are set up before that stream opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,8 +68,9 @@ private:
|
||||
|
||||
class AudioLine : public oboe::AudioStreamDataCallback, public oboe::AudioStreamErrorCallback {
|
||||
public:
|
||||
AudioLine(bool input, int32_t deviceId, int32_t channels)
|
||||
: input_(input), deviceId_(deviceId), channels_(channels),
|
||||
/** {@code preset} is the input preset of a capture line, {@code usage} a playback line's usage. */
|
||||
AudioLine(bool input, int32_t deviceId, int32_t channels, int32_t preset, int32_t usage)
|
||||
: input_(input), deviceId_(deviceId), channels_(channels), preset_(preset), usage_(usage),
|
||||
ring_(static_cast<size_t>(kFrameSize) * channels * (input ? kCaptureFrames : kPlaybackFrames)) {}
|
||||
|
||||
int32_t channels() const { return channels_; }
|
||||
@@ -87,12 +88,12 @@ public:
|
||||
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium)
|
||||
->setDataCallback(this)
|
||||
->setErrorCallback(this);
|
||||
if (deviceId_ > 0) builder.setDeviceId(deviceId_);
|
||||
int32_t deviceId = deviceId_.load();
|
||||
if (deviceId > 0) builder.setDeviceId(deviceId);
|
||||
if (input_) {
|
||||
// The platform's own voice processing stays off: the client runs TS3's.
|
||||
builder.setInputPreset(oboe::InputPreset::VoiceRecognition);
|
||||
builder.setInputPreset(static_cast<oboe::InputPreset>(preset_.load()));
|
||||
} else {
|
||||
builder.setUsage(oboe::Usage::Media)->setContentType(oboe::ContentType::Speech);
|
||||
builder.setUsage(static_cast<oboe::Usage>(usage_.load()))->setContentType(oboe::ContentType::Speech);
|
||||
}
|
||||
std::shared_ptr<oboe::AudioStream> stream;
|
||||
oboe::Result result = builder.openStream(stream);
|
||||
@@ -102,15 +103,35 @@ public:
|
||||
stream->close();
|
||||
return oboe::Result::ErrorClosed;
|
||||
}
|
||||
if (stream_) stream_->close(); // a concurrent reopen got there first
|
||||
stream_ = stream;
|
||||
if (started_) result = stream_->requestStart();
|
||||
return result;
|
||||
}
|
||||
|
||||
oboe::Result start() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
started_ = true;
|
||||
return stream_ ? stream_->requestStart() : oboe::Result::ErrorClosed;
|
||||
}
|
||||
|
||||
/** Moves the line to another device or configuration; the ring and whoever blocks on it stay. */
|
||||
oboe::Result reopen(int32_t deviceId, int32_t preset, int32_t usage) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (closing_.load()) return oboe::Result::ErrorClosed;
|
||||
if (stream_) {
|
||||
stream_->stop();
|
||||
stream_->close();
|
||||
stream_.reset();
|
||||
}
|
||||
}
|
||||
deviceId_.store(deviceId);
|
||||
preset_.store(preset);
|
||||
usage_.store(usage);
|
||||
return open();
|
||||
}
|
||||
|
||||
/** Blocks until {@code count} samples are captured; fewer only once closing. */
|
||||
size_t read(int16_t *data, size_t count) {
|
||||
size_t done = 0;
|
||||
@@ -157,18 +178,21 @@ public:
|
||||
if (closing_.load()) return;
|
||||
__android_log_print(ANDROID_LOG_INFO, kTag, "stream closed (%s), reopening",
|
||||
oboe::convertToText(error));
|
||||
deviceId_ = 0;
|
||||
if (open() == oboe::Result::OK) start();
|
||||
deviceId_.store(0);
|
||||
open();
|
||||
}
|
||||
|
||||
private:
|
||||
const bool input_;
|
||||
int32_t deviceId_;
|
||||
std::atomic<int32_t> deviceId_;
|
||||
const int32_t channels_;
|
||||
std::atomic<int32_t> preset_;
|
||||
std::atomic<int32_t> usage_;
|
||||
SampleRing ring_;
|
||||
std::atomic<bool> closing_{false};
|
||||
std::mutex mutex_;
|
||||
std::shared_ptr<oboe::AudioStream> stream_;
|
||||
bool started_ = false; // guarded by mutex_
|
||||
};
|
||||
|
||||
AudioLine *line(jlong handle) {
|
||||
@@ -183,8 +207,8 @@ extern "C" {
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_ts3client_android_audio_OboeLine_nativeOpen(JNIEnv *env, jclass, jboolean input, jint deviceId,
|
||||
jint channels) {
|
||||
auto *created = new AudioLine(input, deviceId, channels);
|
||||
jint channels, jint preset, jint usage) {
|
||||
auto *created = new AudioLine(input, deviceId, channels, preset, usage);
|
||||
oboe::Result result = created->open();
|
||||
if (result != oboe::Result::OK) {
|
||||
delete created;
|
||||
@@ -207,6 +231,15 @@ Java_com_ts3client_android_audio_OboeLine_nativeStart(JNIEnv *env, jclass, jlong
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_ts3client_android_audio_OboeLine_nativeReopen(JNIEnv *env, jclass, jlong handle, jint deviceId,
|
||||
jint preset, jint usage) {
|
||||
oboe::Result result = line(handle)->reopen(deviceId, preset, usage);
|
||||
if (result != oboe::Result::OK && result != oboe::Result::ErrorClosed) {
|
||||
env->ThrowNew(env->FindClass("java/lang/IllegalStateException"), oboe::convertToText(result));
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_ts3client_android_audio_OboeLine_nativeRead(JNIEnv *env, jclass, jlong handle, jbyteArray buffer,
|
||||
jint offset, jint length) {
|
||||
|
||||
Reference in New Issue
Block a user