Ring Daemon
Loading...
Searching...
No Matches
aaudiolayer.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#include "aaudiolayer.h"
19#include "logger.h"
20
21#include <aaudio/AAudio.h>
22#include <dlfcn.h>
23
24#include <string>
25
26namespace jami {
27
28// Signature for functions not available on older Android versions
32
33// Set once from JNI_OnLoad; used to create the Java AudioTrack fallback on API < 28.
34static JavaVM* sJavaVM {nullptr};
35
42
43void
48
50{
51 {
52 std::lock_guard lk(mutex_);
53 isRunning_ = false;
54 loopCv_.notify_all();
55 }
56 if (loopThread_.joinable())
57 loopThread_.join();
59}
60
61void
62AAudioLayer::loop()
63{
64 std::unique_lock lk(mutex_);
65 while (true) {
66 loopCv_.wait(lk, [this] { return not isRunning_ or not streamsToRestart_.empty(); });
67 if (!isRunning_)
68 break;
69 auto streams = std::move(streamsToRestart_);
70 for (auto stream : streams) {
71 JAMI_WARNING("Restarting stream type {} after disconnection", (unsigned) stream);
72 stopStreamLocked(stream);
73 startStreamLocked(stream);
74 }
75 }
76}
77
78AudioFormat
88
89AAudioLayer::AAudioStreamPtr
90AAudioLayer::buildStream(AudioDeviceType type) {
97 // type == AudioDeviceType::RINGTONE ? AAUDIO_SHARING_MODE_SHARED
98 // : AAUDIO_SHARING_MODE_EXCLUSIVE);
103
104 static auto setUsage = reinterpret_cast<SetUsageFunc>(dlsym(RTLD_DEFAULT,
105 "AAudioStreamBuilder_setUsage"));
106 static auto setContentType = reinterpret_cast<SetContentTypeFunc>(dlsym(RTLD_DEFAULT,
107 "AAudioStreamBuilder_setContentType"));
108 static auto setInputPreset = reinterpret_cast<SetInputPresetFunc>(dlsym(RTLD_DEFAULT,
109 "AAudioStreamBuilder_setInputPreset"));
110 if (setUsage) {
114 } else {
115 JAMI_WARNING("AAudioStreamBuilder_setUsage not available, stream usage will be unknown");
116 }
117 if (setContentType) {
121 } else {
123 "AAudioStreamBuilder_setContentType not available, stream content type will be unknown");
124 }
125 if (type == AudioDeviceType::CAPTURE) {
126 if (setInputPreset) {
128 } else {
129 JAMI_WARNING( "AAudioStreamBuilder_setInputPreset not available, input preset will be unknown");
130 }
131 }
132
133 AAudioStreamBuilder_setDataCallback(builder, dataCallback, this);
134 AAudioStreamBuilder_setErrorCallback(builder, errorCallback, this);
135
136 AAudioStream* streamptr = nullptr;
138 if (result != AAUDIO_OK && type != AudioDeviceType::RINGTONE) {
139 JAMI_ERROR("Error opening {} stream: {}. Retrying with shared.",
140 (type == AudioDeviceType::CAPTURE) ? "capture" : "playback",
144 }
146 if (result != AAUDIO_OK) {
147 JAMI_ERROR("Error opening {} stream (shared): {}",
148 (type == AudioDeviceType::CAPTURE) ? "capture" : "playback",
150 return nullptr;
151 }
152 return AAudioStreamPtr(streamptr);
153}
154
155void
157{
158 std::lock_guard lock(mutex_);
159 startStreamLocked(stream);
160}
161
162void
163AAudioLayer::startStreamLocked(AudioDeviceType stream)
164{
165 JAMI_WARNING("Starting AAudio layer for stream type {}", (unsigned) stream);
167
168 // Playback
169 if (stream == AudioDeviceType::PLAYBACK) {
170 if (playStream_)
171 return;
172 playStream_ = buildStream(AudioDeviceType::PLAYBACK);
173 if (!playStream_) {
174 JAMI_ERROR("Failed to create playback stream");
175 return;
176 }
177 AudioFormat format = getStreamFormat(playStream_.get());
178
179 // Start with minimal buffer size (low latency) and let the callback increase it if underruns occur.
180 AAudioStream_setBufferSizeInFrames(playStream_.get(), AAudioStream_getFramesPerBurst(playStream_.get()) * 2);
181 //AAudioStream_setBufferSizeInFrames(playStream_.get(), 20 * format.sample_rate / 1000);
182 auto bufferSize = AAudioStream_getBufferCapacityInFrames(playStream_.get());
184
185 // Request start
186 auto result = AAudioStream_requestStart(playStream_.get());
187 if (result == AAUDIO_OK) {
188 JAMI_WARNING("Playback stream started with format: {}, buffer size: {} frames",
189 format.toString(),
190 bufferSize);
192 playbackChanged(true);
193 } else {
194 JAMI_ERROR("Error starting playback stream: {}", AAudio_convertResultToText(result));
195 }
196 } else if (stream == AudioDeviceType::RINGTONE) {
197 if (ringStream_ || javaRingTrack_)
198 return;
199
200 // On API < 28, AAudioStreamBuilder_setUsage is unavailable so the stream
201 // would default to AUDIO_STREAM_MUSIC. Build a Java AudioTrack with
202 // STREAM_RING directly so that DND / silent / vibrate rules are respected
203 // and the hardware volume keys control ring volume, not media volume.
204 static auto setUsageCheck = reinterpret_cast<SetUsageFunc>(
205 dlsym(RTLD_DEFAULT, "AAudioStreamBuilder_setUsage"));
206 if (!setUsageCheck) {
207 if (!startJavaRingStream()) {
208 JAMI_ERROR("Failed to start Java ringtone stream fallback");
209 }
210 return;
211 }
212
213 ringStream_ = buildStream(AudioDeviceType::RINGTONE);
214 if (!ringStream_) {
215 JAMI_ERROR("Error opening ringtone stream");
216 return;
217 }
218 AudioFormat format = getStreamFormat(ringStream_.get());
219 AAudioStream_setBufferSizeInFrames(ringStream_.get(), 20 * format.sample_rate / 1000);
220 auto bufferSize = AAudioStream_getBufferCapacityInFrames(ringStream_.get());
222
223 auto result = AAudioStream_requestStart(ringStream_.get());
224 if (result == AAUDIO_OK) {
225 JAMI_WARNING("Ringtone stream started with format: {}, buffer size: {} frames",
226 format.toString(),
227 bufferSize);
229 playbackChanged(true);
230 } else {
231 JAMI_ERROR("Error starting ringtone stream: {}", AAudio_convertResultToText(result));
232 }
233 } else if (stream == AudioDeviceType::CAPTURE) {
234 if (recStream_)
235 return;
236 recStream_ = buildStream(AudioDeviceType::CAPTURE);
237 if (!recStream_) {
238 JAMI_ERROR("Error opening capture stream");
239 return;
240 }
241 auto result = AAudioStream_requestStart(recStream_.get());
242 if (result == AAUDIO_OK) {
243 AudioFormat format = getStreamFormat(recStream_.get());
245 JAMI_WARNING("Capture stream started with format: {}", format.toString());
246 recordChanged(true);
247 } else {
248 JAMI_ERROR("Error starting capture stream: {}", AAudio_convertResultToText(result));
249 }
250 }
251
252 if (!isRunning_) {
253 isRunning_ = true;
254 loopThread_ = std::thread([this] { loop(); });
255 }
256}
257
258void
260{
261 std::lock_guard lock(mutex_);
262 if (stream == AudioDeviceType::ALL) {
263 streamsToRestart_.clear();
264 } else {
265 streamsToRestart_.erase(stream);
266 }
267 stopStreamLocked(stream);
268}
269
270void
271AAudioLayer::stopStreamLocked(AudioDeviceType stream)
272{
273 JAMI_WARNING("Stopping AAudio layer for stream type {}", (unsigned) stream);
274
275 if (stream == AudioDeviceType::PLAYBACK || stream == AudioDeviceType::ALL) {
276 if (playStream_) {
277 AAudioStream_requestStop(playStream_.get());
278 playStream_.reset();
279 playbackChanged(false);
280 }
281 }
282
283 if (stream == AudioDeviceType::RINGTONE || stream == AudioDeviceType::ALL) {
284 if (javaRingTrack_) {
285 stopJavaRingStream();
286 } else if (ringStream_) {
287 AAudioStream_requestStop(ringStream_.get());
288 ringStream_.reset();
289 }
290 }
291
292 if (stream == AudioDeviceType::CAPTURE || stream == AudioDeviceType::ALL) {
293 if (recStream_) {
294 AAudioStream_requestStop(recStream_.get());
295 recStream_.reset();
296 recordChanged(false);
297 }
298 }
300}
301
303AAudioLayer::dataCallback(AAudioStream* stream, void* userData, void* audioData, int32_t numFrames)
304{
305 AAudioLayer* layer = static_cast<AAudioLayer*>(userData);
306 if (!layer)
308
309 auto direction = AAudioStream_getDirection(stream);
310 auto format = getStreamFormat(stream);
311 int32_t numSamples = numFrames * format.nb_channels;
312
313 if (direction == AAUDIO_DIRECTION_OUTPUT) {
314 // JAMI_WARNING("Playback callback: {} frames, format: {}, sample rate: {}", numFrames, (aaFormat ==
315 // AAUDIO_FORMAT_PCM_FLOAT) ? "Float" : "I16", sampleRate);
316
317 // Optimize buffer size (Check for underruns)
319 if (underrunCount > layer->previousUnderrunCount_) {
320 layer->previousUnderrunCount_ = underrunCount;
324
327 JAMI_WARNING("Underrun detected (count: {}), increasing buffer size to {} frames",
329 bufferSize + burst);
330 }
331 }
332
333 auto frame = stream == layer->ringStream_.get() ? layer->getToRing(format, numFrames)
334 : layer->getToPlay(format, numFrames);
335 if (frame && frame->pointer() && frame->pointer()->data[0]) {
336 float* output = static_cast<float*>(audioData);
337 const float* src = reinterpret_cast<const float*>(frame->pointer()->data[0]);
338 std::copy(src, src + numSamples, output);
339 } else {
340 JAMI_WARNING("Playback underflow: no data available, filling with silence");
341 std::fill_n(static_cast<float*>(audioData), numSamples, 0.0f);
342 }
343 } else if (direction == AAUDIO_DIRECTION_INPUT) { // Capture
344 auto out = std::make_shared<AudioFrame>(format, numFrames);
345 if (out->pointer() && out->pointer()->data[0]) {
346 auto* dst = reinterpret_cast<float*>(out->pointer()->data[0]);
347 const auto* src = static_cast<const float*>(audioData);
348 std::copy(src, src + numSamples, dst);
349 layer->putRecorded(std::move(out));
350 }
351 }
352
354}
355
356void
357AAudioLayer::errorCallback(AAudioStream* stream, void* userData, aaudio_result_t error)
358{
359 AAudioLayer* layer = static_cast<AAudioLayer*>(userData);
360 auto streamType = (stream == layer->playStream_.get()) ? AudioDeviceType::PLAYBACK
361 : (stream == layer->recStream_.get()) ? AudioDeviceType::CAPTURE
363 JAMI_ERROR("AAudio error: {} for type {}", AAudio_convertResultToText(error), (unsigned) streamType);
364
365 if (error == AAUDIO_ERROR_DISCONNECTED) {
366 std::lock_guard lk(layer->mutex_);
367 layer->streamsToRestart_.insert(streamType);
368 layer->loopCv_.notify_one();
369 }
370}
371
372std::vector<std::string>
374{
375 return {"Default"}; // Helper to properly list devices using Android Java API or Oboe later
376}
377
378// ---------------------------------------------------------------------------
379// Java AudioTrack fallback for ringtone on Android API < 28
380// ---------------------------------------------------------------------------
381
382static constexpr int JAVA_STREAM_RING = 2; // AudioManager.STREAM_RING
383static constexpr int JAVA_CHANNEL_OUT_STEREO = 12; // AudioFormat.CHANNEL_OUT_STEREO
384static constexpr int JAVA_ENCODING_PCM_FLOAT = 4; // AudioFormat.ENCODING_PCM_FLOAT (API 21+)
385static constexpr int JAVA_MODE_STREAM = 1; // AudioTrack.MODE_STREAM
386static constexpr int JAVA_STATE_INITIALIZED = 1; // AudioTrack.STATE_INITIALIZED
387static constexpr int JAVA_WRITE_BLOCKING = 0; // AudioTrack.WRITE_BLOCKING
388static constexpr int JAVA_RING_SAMPLE_RATE = 48000;
389static constexpr int JAVA_RING_CHANNELS = 2;
390// 10 ms of stereo float at 48 kHz
391static constexpr int JAVA_RING_FRAMES = 480;
393
394bool
395AAudioLayer::startJavaRingStream()
396{
397 if (!sJavaVM) {
398 JAMI_ERROR("AAudioLayer: no JavaVM — cannot create Java ringtone AudioTrack");
399 return false;
400 }
401
402 JNIEnv* env = nullptr;
403 bool attached = false;
404 jint envStatus = sJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
405 if (envStatus == JNI_EDETACHED) {
406 if (sJavaVM->AttachCurrentThread(&env, nullptr) == JNI_OK)
407 attached = true;
408 else {
409 JAMI_ERROR("AAudioLayer: failed to attach thread for ringtone stream creation");
410 return false;
411 }
412 } else if (envStatus != JNI_OK) {
413 JAMI_ERROR("AAudioLayer: cannot obtain JNIEnv for ringtone stream creation");
414 return false;
415 }
416
417 jclass atClass = env->FindClass("android/media/AudioTrack");
418 if (!atClass) {
419 JAMI_ERROR("AAudioLayer: AudioTrack class not found");
420 if (attached) sJavaVM->DetachCurrentThread();
421 return false;
422 }
423
424 // int AudioTrack.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
425 jmethodID minBufMethod = env->GetStaticMethodID(atClass, "getMinBufferSize", "(III)I");
426 jint minBytes = env->CallStaticIntMethod(atClass,
431 if (minBytes <= 0)
432 minBytes = JAVA_RING_FLOATS * static_cast<int>(sizeof(float)) * 2;
433 // Use 2× minimum so there is always headroom without introducing latency.
434 jint bufBytes = minBytes * 2;
435
436 // AudioTrack(int streamType, int sampleRate, int channelConfig,
437 // int audioFormat, int bufferSizeInBytes, int mode)
438 jmethodID ctor = env->GetMethodID(atClass, "<init>", "(IIIIII)V");
439 jobject track = env->NewObject(atClass,
440 ctor,
445 bufBytes,
447 if (!track || env->ExceptionCheck()) {
448 env->ExceptionClear();
449 JAMI_ERROR("AAudioLayer: failed to construct Java AudioTrack for ringtone");
450 env->DeleteLocalRef(atClass);
451 if (attached) sJavaVM->DetachCurrentThread();
452 return false;
453 }
454
455 // Verify the track initialised correctly.
456 jmethodID getState = env->GetMethodID(atClass, "getState", "()I");
457 if (env->CallIntMethod(track, getState) != JAVA_STATE_INITIALIZED) {
458 JAMI_ERROR("AAudioLayer: Java AudioTrack not initialised (wrong state)");
459 env->DeleteLocalRef(track);
460 env->DeleteLocalRef(atClass);
461 if (attached) sJavaVM->DetachCurrentThread();
462 return false;
463 }
464
465 jmethodID playMethod = env->GetMethodID(atClass, "play", "()V");
466 env->CallVoidMethod(track, playMethod);
467
468 env->DeleteLocalRef(atClass);
469
470 // Promote to global refs so the write thread can access them safely.
471 javaRingTrack_ = env->NewGlobalRef(track);
472 jfloatArray buf = env->NewFloatArray(JAVA_RING_FLOATS);
473 javaRingBuffer_ = reinterpret_cast<jfloatArray>(env->NewGlobalRef(buf));
474 env->DeleteLocalRef(track);
475 env->DeleteLocalRef(buf);
476
477 if (attached) sJavaVM->DetachCurrentThread();
478
479 // Tell the daemon what format to expect from getToRing().
481 hardwareFormatAvailable(format, bufBytes / static_cast<int>(sizeof(float)));
482
483 javaRingRunning_ = true;
484 javaRingThread_ = std::thread([this] { javaRingLoop(); });
485
486 JAMI_WARNING("AAudioLayer: Java STREAM_RING AudioTrack started (API < 28 fallback)");
488 playbackChanged(true);
489 return true;
490}
491
492void
493AAudioLayer::javaRingLoop()
494{
495 if (!sJavaVM)
496 return;
497
498 JNIEnv* env = nullptr;
499 bool attached = false;
500 jint envStatus = sJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
501 if (envStatus == JNI_EDETACHED) {
502 if (sJavaVM->AttachCurrentThread(&env, nullptr) == JNI_OK)
503 attached = true;
504 else {
505 JAMI_ERROR("AAudioLayer: ring thread failed to attach to JVM");
506 return;
507 }
508 }
509
510 jclass atClass = env->GetObjectClass(javaRingTrack_);
511 // write(float[] audioData, int offsetInFloats, int sizeInFloats, int writeMode) → int
512 jmethodID writeMethod = env->GetMethodID(atClass, "write", "([FIII)I");
513 env->DeleteLocalRef(atClass);
514
516
517 while (javaRingRunning_) {
518 auto frame = getToRing(format, JAVA_RING_FRAMES);
519 if (frame && frame->pointer() && frame->pointer()->data[0]) {
520 const auto* src = reinterpret_cast<const float*>(frame->pointer()->data[0]);
521 env->SetFloatArrayRegion(javaRingBuffer_, 0, JAVA_RING_FLOATS, src);
522 env->CallIntMethod(javaRingTrack_,
524 javaRingBuffer_,
525 0,
528 } else {
529 // No audio ready yet — yield briefly to avoid busy-spinning.
530 std::this_thread::sleep_for(std::chrono::milliseconds(5));
531 }
532 }
533
534 if (attached)
535 sJavaVM->DetachCurrentThread();
536}
537
538void
539AAudioLayer::stopJavaRingStream()
540{
541 javaRingRunning_ = false;
542 if (javaRingThread_.joinable())
543 javaRingThread_.join();
544
545 if (!sJavaVM || !javaRingTrack_)
546 return;
547
548 JNIEnv* env = nullptr;
549 bool attached = false;
550 if (sJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
551 if (sJavaVM->AttachCurrentThread(&env, nullptr) == JNI_OK)
552 attached = true;
553 }
554
555 if (env) {
556 jclass atClass = env->GetObjectClass(javaRingTrack_);
557 jmethodID stopMethod = env->GetMethodID(atClass, "stop", "()V");
558 jmethodID releaseMethod= env->GetMethodID(atClass, "release", "()V");
559 env->CallVoidMethod(javaRingTrack_, stopMethod);
560 env->CallVoidMethod(javaRingTrack_, releaseMethod);
561 env->DeleteLocalRef(atClass);
562
563 env->DeleteGlobalRef(javaRingTrack_);
564 env->DeleteGlobalRef(reinterpret_cast<jobject>(javaRingBuffer_));
565 }
566
567 javaRingTrack_ = nullptr;
568 javaRingBuffer_ = nullptr;
569
570 if (attached)
571 sJavaVM->DetachCurrentThread();
572
573 playbackChanged(false);
574 JAMI_WARNING("AAudioLayer: Java STREAM_RING AudioTrack stopped");
575}
576
577std::vector<std::string>
579{
580 return {"Default"};
581}
582
583int
585{
586 return 0;
587}
588
589std::string
591{
592 return "Default";
593}
594
595void
598
599} // namespace jami
void updatePreference(AudioPreference &pref, int index, AudioDeviceType type) override
void startStream(AudioDeviceType stream) override
Start the capture stream and prepare the playback stream.
static void setJavaVM(JavaVM *vm)
AAudioLayer(const AudioPreference &pref)
std::string getAudioDeviceName(int index, AudioDeviceType type) const override
int getAudioDeviceIndex(const std::string &name, AudioDeviceType type) const override
std::vector< std::string > getPlaybackDeviceList() const override
std::vector< std::string > getCaptureDeviceList() const override
void stopStream(AudioDeviceType stream=AudioDeviceType::ALL) override
Stop the playback and capture streams.
std::mutex mutex_
Lock for the entire audio layer.
Definition audiolayer.h:296
std::atomic< Status > status_
Whether or not the audio layer's playback stream is started.
Definition audiolayer.h:273
void playbackChanged(bool started)
void setHasNativeNS(bool hasNS)
void hardwareFormatAvailable(AudioFormat playback, size_t bufSize=0)
Callback to be called by derived classes when the audio output is opened.
std::shared_ptr< AudioFrame > getToRing(AudioFormat format, size_t writableSamples)
void hardwareInputFormatAvailable(AudioFormat capture)
Set the input format on necessary objects.
void setHasNativeAEC(bool hasEAC)
void recordChanged(bool started)
#define JAMI_ERROR(formatstr,...)
Definition logger.h:243
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
void(*)(AAudioStreamBuilder *, aaudio_input_preset_t) SetInputPresetFunc
static constexpr int JAVA_STATE_INITIALIZED
static constexpr int JAVA_STREAM_RING
void(*)(AAudioStreamBuilder *, aaudio_usage_t) SetUsageFunc
void emitSignal(Args... args)
Definition jami_signal.h:64
static JavaVM * sJavaVM
static constexpr int JAVA_RING_CHANNELS
static constexpr int JAVA_CHANNEL_OUT_STEREO
static constexpr int JAVA_RING_SAMPLE_RATE
static constexpr int JAVA_MODE_STREAM
AudioDeviceType
Definition audiolayer.h:57
static constexpr int JAVA_RING_FRAMES
static constexpr int JAVA_ENCODING_PCM_FLOAT
void(*)(AAudioStreamBuilder *, aaudio_content_type_t) SetContentTypeFunc
static constexpr int JAVA_RING_FLOATS
static constexpr int JAVA_WRITE_BLOCKING
AudioFormat getStreamFormat(AAudioStream *stream)
Structure to hold sample rate and channel number associated with audio data.