Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
QtAudioDeviceManager.java
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4package org.qtproject.qt.android.multimedia;
5
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.Comparator;
9import java.util.HashMap;
10import java.util.List;
11import java.util.concurrent.atomic.AtomicInteger;
12import android.content.Context;
13import android.media.AudioDeviceCallback;
14import android.media.AudioDeviceInfo;
15import android.media.AudioFormat;
16import android.media.AudioManager;
17import android.media.AudioRecord;
18import android.media.AudioTrack;
19import android.media.MediaRecorder;
20import android.os.Build;
21import android.os.Handler;
22import android.os.Looper;
23import android.util.Log;
24import android.util.Range;
25
26import org.qtproject.qt.android.UsedFromNativeCode;
27
28class QtAudioDeviceManager
29{
30 private static final String TAG = "QtAudioDeviceManager";
31
32 static private AudioManager m_audioManager = null;
33 static private final AudioDevicesReceiver m_audioDevicesReceiver = new AudioDevicesReceiver();
34 static private Handler handler = new Handler(Looper.getMainLooper());
35 static private AudioRecord m_recorder = null;
36 static private AudioTrack m_streamPlayer = null;
37 static private Thread m_streamingThread = null;
38 static private boolean m_isStreaming = false;
39 static private boolean m_useSpeaker = false;
40 static private final int m_sampleRate = 8000;
41 static private final int m_channels = AudioFormat.CHANNEL_CONFIGURATION_MONO;
42 static private final int m_audioFormat = AudioFormat.ENCODING_PCM_16BIT;
43 static private final int m_bufferSize = AudioRecord.getMinBufferSize(m_sampleRate, m_channels, m_audioFormat);
44 static private int m_currentOutputId = -1;
45 static private AtomicInteger m_scoCounter = new AtomicInteger();
46 static private AtomicInteger m_communicationDeviceCounter = new AtomicInteger();
47 static private AtomicInteger m_speakerphoneCounter = new AtomicInteger();
48 static private int m_currentCommunicationDeviceId = -1;
49
50 static native void onAudioInputDevicesUpdated();
51 static native void onAudioOutputDevicesUpdated();
52
53 static private void updateDeviceList() {
54 if (m_currentOutputId != -1)
55 prepareAudioOutput(m_currentOutputId);
56 onAudioInputDevicesUpdated();
57 onAudioOutputDevicesUpdated();
58 }
59
60 private static class AudioDevicesReceiver extends AudioDeviceCallback {
61 @Override
62 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
63 updateDeviceList();
64 }
65
66 @Override
67 public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
68 updateDeviceList();
69 }
70 }
71
72
73 static void registerAudioHeadsetStateReceiver()
74 {
75 m_audioManager.registerAudioDeviceCallback(m_audioDevicesReceiver, handler);
76 }
77
78 static void unregisterAudioHeadsetStateReceiver()
79 {
80 m_audioManager.unregisterAudioDeviceCallback(m_audioDevicesReceiver);
81 }
82
83 static void setContext(Context context)
84 {
85 m_audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
86 }
87
88 static AudioDeviceInfo[] getAudioOutputDevices()
89 {
90 return getAudioDevices(AudioManager.GET_DEVICES_OUTPUTS);
91 }
92
93 static AudioDeviceInfo[] getAudioInputDevices()
94 {
95 return getAudioDevices(AudioManager.GET_DEVICES_INPUTS);
96 }
97
98 @UsedFromNativeCode
99 static int getDefaultSampleRate()
100 {
101 String sampleRate = m_audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
102 return Integer.parseInt(sampleRate);
103 }
104
105 @UsedFromNativeCode
106 static boolean isBluetoothDevice(AudioDeviceInfo deviceInfo)
107 {
108 switch (deviceInfo.getType()) {
109 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
110 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
111 return true;
112 default:
113 return false;
114 }
115 }
116
117 // FIXME: It's quite misleading to "set" audio input/output when no MediaRecorder is used. We
118 // should more likely be "preparing" Android for the use of a specific audio device in a
119 // desired mode
120 @UsedFromNativeCode
121 static boolean prepareAudioInput(int id)
122 {
123 final AudioDeviceInfo[] audioDevices =
124 m_audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
125 for (AudioDeviceInfo deviceInfo : audioDevices) {
126 if (deviceInfo.getId() == id) {
127 switch (deviceInfo.getType())
128 {
129 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
130 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
131 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
132 case AudioDeviceInfo.TYPE_USB_HEADSET:
133 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
134 return prepareAudioDevice(deviceInfo, AudioManager.MODE_NORMAL);
135 default:
136 return true;
137 }
138 }
139 }
140 return false;
141 }
142
143 private static void setInputMuted(boolean mute)
144 {
145 // This method mutes the microphone across the entire platform
146 m_audioManager.setMicrophoneMute(mute);
147 }
148
149 private static boolean isMicrophoneMute()
150 {
151 return m_audioManager.isMicrophoneMute();
152 }
153
154 private static String audioDeviceTypeToString(int type)
155 {
156 // API <= 23 types
157 switch (type)
158 {
159 case AudioDeviceInfo.TYPE_AUX_LINE:
160 return "AUX Line";
161 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
162 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
163 return "Bluetooth";
164 case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
165 return "Built in earpiece";
166 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
167 return "Built in microphone";
168 case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
169 return "Built in speaker";
170 case AudioDeviceInfo.TYPE_DOCK:
171 return "Dock";
172 case AudioDeviceInfo.TYPE_FM:
173 return "FM";
174 case AudioDeviceInfo.TYPE_FM_TUNER:
175 return "FM TUNER";
176 case AudioDeviceInfo.TYPE_HDMI:
177 return "HDMI";
178 case AudioDeviceInfo.TYPE_HDMI_ARC:
179 return "HDMI ARC";
180 case AudioDeviceInfo.TYPE_IP:
181 return "IP";
182 case AudioDeviceInfo.TYPE_LINE_ANALOG:
183 return "Line analog";
184 case AudioDeviceInfo.TYPE_LINE_DIGITAL:
185 return "Line digital";
186 case AudioDeviceInfo.TYPE_TV_TUNER:
187 return "TV tuner";
188 case AudioDeviceInfo.TYPE_USB_ACCESSORY:
189 return "USB accessory";
190 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
191 return "Wired headphones";
192 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
193 return "Wired headset";
194 }
195
196 // API 24
197 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
198 if (type == AudioDeviceInfo.TYPE_BUS)
199 return "Bus";
200 }
201
202 return "Unknown-Type";
203
204 }
205
206 private static final HashMap<Integer, Integer> priorityMap = new HashMap<Integer, Integer>() {{
207 put(AudioDeviceInfo.TYPE_WIRED_HEADSET, 1);
208 put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, 1);
209 put(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 2);
210 put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, 2);
211 put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, 3);
212 }};
213 private static final int DEFAULT_PRIORITY = 4;
214
215 private static void sortAudioDevices(AudioDeviceInfo[] devices) {
216 Comparator<AudioDeviceInfo> deviceTypeComparator = new Comparator<AudioDeviceInfo>() {
217 @Override
218 public int compare(AudioDeviceInfo device1, AudioDeviceInfo device2) {
219 return getPriority(device1) - getPriority(device2);
220 }
221
222 private int getPriority(AudioDeviceInfo device) {
223 return priorityMap.getOrDefault(device.getType(), DEFAULT_PRIORITY);
224 }
225 };
226
227 Arrays.sort(devices, deviceTypeComparator);
228 }
229
230 private static AudioDeviceInfo[] getAudioDevices(int type)
231 {
232 ArrayList<AudioDeviceInfo> filteredDevices = new ArrayList<>();
233
234 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
235 boolean builtInMicAdded = false;
236 boolean bluetoothDeviceAdded = false;
237 AudioDeviceInfo[] audioDevices = m_audioManager.getDevices(type);
238 sortAudioDevices(audioDevices);
239
240 for (AudioDeviceInfo deviceInfo : audioDevices) {
241 String deviceType = audioDeviceTypeToString(deviceInfo.getType());
242
243 if (deviceType.equals(audioDeviceTypeToString(AudioDeviceInfo.TYPE_UNKNOWN))) {
244 // Not supported device type
245 continue;
246 } else if (deviceType.equals(audioDeviceTypeToString(AudioDeviceInfo.TYPE_BUILTIN_MIC))) {
247 if (builtInMicAdded) {
248 // Built in mic already added. Second built in mic is CAMCORDER, but there
249 // is no reliable way of selecting it. AudioSource.MIC usually means the
250 // primary mic. AudioSource.CAMCORDER source might mean the secondary mic,
251 // but there's no guarantee. It depends e.g. on the physical placement
252 // of the mics. That's why we will not add built in microphone twice.
253 // Should we?
254 continue;
255 }
256 builtInMicAdded = true;
257 } else if (isBluetoothDevice(deviceInfo)) {
258 if (bluetoothDeviceAdded) {
259 // Bluetooth device already added. Second device is just a different
260 // technology profille (like A2DP or SCO). We should not add the same
261 // device twice. Should we?
262 continue;
263 }
264 bluetoothDeviceAdded = true;
265 }
266
267 filteredDevices.add(deviceInfo);
268 }
269 }
270
271 return filteredDevices.toArray(new AudioDeviceInfo[filteredDevices.size()]);
272 }
273
274 final private static int [] bluetoothTypes = {
275 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
276 AudioDeviceInfo.TYPE_BLUETOOTH_SCO
277 };
278 final private static int [] wiredTypes = {
279 AudioDeviceInfo.TYPE_WIRED_HEADSET,
280 AudioDeviceInfo.TYPE_WIRED_HEADPHONES
281 };
282
283 private static boolean containsAnyOfType(AudioDeviceInfo[] devices, int[]... types) {
284
285 return Arrays.stream(devices)
286 .anyMatch(device -> Arrays.stream(types)
287 .flatMapToInt(Arrays::stream)
288 .anyMatch(type -> device.getType() == type));
289 }
290
291 private static int getCorrectModeIfContainsAnyOfType(AudioDeviceInfo[] devices, int[]... types) {
292 return containsAnyOfType(devices, types) ?
293 AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL;
294 }
295
296 private static int getModeForWired(AudioDeviceInfo[] audioDevices)
297 {
298 return getCorrectModeIfContainsAnyOfType(audioDevices, bluetoothTypes);
299 }
300
301 private static int getModeForBluetooth(AudioDeviceInfo[] audioDevices)
302 {
303 return getCorrectModeIfContainsAnyOfType(audioDevices, wiredTypes);
304 }
305
306 private static int getModeForSpeaker(AudioDeviceInfo[] audioDevices)
307 {
308 return getCorrectModeIfContainsAnyOfType(audioDevices, bluetoothTypes, wiredTypes);
309 }
310
311 private static boolean prepareAudioOutput(int id)
312 {
313 final AudioDeviceInfo[] audioDevices =
314 m_audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
315 for (AudioDeviceInfo deviceInfo : audioDevices) {
316 if (deviceInfo.getId() == id) {
317 switch (deviceInfo.getType())
318 {
319 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
320 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
321 return prepareAudioDevice(deviceInfo, getModeForBluetooth(audioDevices));
322 case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
323 return prepareAudioDevice(deviceInfo, getModeForSpeaker(audioDevices));
324 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
325 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
326 return prepareAudioDevice(deviceInfo, getModeForWired(audioDevices));
327 case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
328 // It doesn't work when WIRED HEADPHONES are connected
329 // Earpiece has the lowest priority and setWiredHeadsetOn(boolean)
330 // method to force it is deprecated
331 Log.w(TAG, "Built in Earpiece may not work when "
332 + "Wired Headphones are connected");
333 return prepareAudioDevice(deviceInfo, AudioManager.MODE_IN_CALL);
334 case AudioDeviceInfo.TYPE_HDMI:
335 case AudioDeviceInfo.TYPE_HDMI_ARC:
336 case AudioDeviceInfo.TYPE_HDMI_EARC:
337 return prepareAudioDevice(deviceInfo, AudioManager.MODE_NORMAL);
338 default:
339 return false;
340 }
341 }
342 }
343 return false;
344 }
345
350 private static AudioDeviceInfo getValidCommunicationDevice(AudioDeviceInfo device) {
351 if (device.isSink())
352 return device;
353
354 if (isBluetoothDevice(device)) {
355 // For Bluetooth sources, get output device with the same type and address
356 List<AudioDeviceInfo> communicationDevices = m_audioManager.getAvailableCommunicationDevices();
357 for (AudioDeviceInfo communicationDevice : communicationDevices) {
358 boolean isSameType = communicationDevice.getType() == device.getType();
359 boolean isSameAddress = communicationDevice.getAddress().equals(device.getAddress());
360 if (isSameType && isSameAddress)
361 return communicationDevice;
362 }
363
364 Log.w(TAG, "No matching bluetooth output device found for " + device);
365 }
366
367 return null;
368 }
369
370 private static boolean deviceIsCurrentCommunicationDevice(AudioDeviceInfo device) {
371 if (m_currentCommunicationDeviceId == -1 || device == null) {
372 return false;
373 }
374
375 return m_currentCommunicationDeviceId == device.getId();
376 }
377
378 @UsedFromNativeCode
379 static void releaseAudioDevice(int id) {
380 final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
382 if (device.getId() == id)
383 releaseAudioDevice(device);
384 }
385 }
386
390 private static void releaseAudioDevice(AudioDeviceInfo deviceInfo) {
391 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
392 // If device was used as the communication device, it should be cleared
393 AudioDeviceInfo communicationDevice = getValidCommunicationDevice(deviceInfo);
394 if (!deviceIsCurrentCommunicationDevice(communicationDevice))
395 return;
396
397 if (m_communicationDeviceCounter.decrementAndGet() == 0) {
398 m_audioManager.clearCommunicationDevice();
399 m_currentCommunicationDeviceId = -1;
400 }
401 } else if (isBluetoothDevice(deviceInfo) && m_audioManager.isBluetoothScoOn()
402 && m_scoCounter.decrementAndGet() == 0) {
403 m_audioManager.stopBluetoothSco();
404 m_audioManager.setBluetoothScoOn(false);
405 } else if (deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
406 && m_speakerphoneCounter.decrementAndGet() == 0) {
407 m_audioManager.setSpeakerphoneOn(false);
408 }
409 }
410
411 private static boolean prepareAudioDevice(AudioDeviceInfo deviceInfo, int mode)
412 {
413 if (deviceInfo == null)
414 return false;
415
416 m_audioManager.setMode(mode);
417
418 boolean isSink = deviceInfo.isSink();
419 if (isSink)
420 m_currentOutputId = deviceInfo.getId();
421
422 boolean isBluetoothDevice = isBluetoothDevice(deviceInfo);
423 boolean isBluetoothSource = isBluetoothDevice && !isSink;
424 boolean isCommunicationMode = (mode == AudioManager.MODE_IN_CALL
425 || mode == AudioManager.MODE_IN_COMMUNICATION);
426
427 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
428 if (!isBluetoothSource && !isCommunicationMode)
429 return true;
430
431 // For communication modes and Bluetooth sources, it's required to set a communication device
432 AudioDeviceInfo communicationDevice = getValidCommunicationDevice(deviceInfo);
433 if (communicationDevice == null) {
434 Log.w(TAG, "No suitable communication device to set to enable communication via "
435 + deviceInfo.getId());
436 return false;
437 }
438
439 if (deviceIsCurrentCommunicationDevice(communicationDevice)) {
440 m_communicationDeviceCounter.incrementAndGet();
441 return true;
442 } else if (m_audioManager.setCommunicationDevice(communicationDevice)) {
443 // NOTE: Keep track of communication devices we set, as it takes time for it to be
444 // fully operational.
445 // TODO: Other applications can set a different communication device, in which case
446 // we should probably register a listener and clear our tracking when the
447 // communication device unexpectedly changes
448 m_currentCommunicationDeviceId = communicationDevice.getId();
449 m_communicationDeviceCounter.set(1);
450 return true;
451 }
452
453 return false;
454 }
455
456 boolean isSpeakerphoneDevice = deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
457
458 if (isBluetoothSource && m_scoCounter.getAndIncrement() == 0) {
459 m_audioManager.startBluetoothSco();
460 m_audioManager.setBluetoothScoOn(true);
461 } else if (isSpeakerphoneDevice
462 && m_speakerphoneCounter.getAndIncrement() == 0) {
463 // TODO: Check if setting speakerphone is actually required for anything, it's not
464 // recommended in Android docs.
465 m_audioManager.setSpeakerphoneOn(true);
466 }
467
468 return true;
469 }
470
471 private static void streamSound()
472 {
473 byte data[] = new byte[m_bufferSize];
474 while (m_isStreaming) {
475 m_recorder.read(data, 0, m_bufferSize);
476 m_streamPlayer.play();
477 m_streamPlayer.write(data, 0, m_bufferSize);
478 m_streamPlayer.stop();
479 }
480 }
481
482 private static void startSoundStreaming(int inputId, int outputId)
483 {
484 if (m_isStreaming)
485 stopSoundStreaming();
486
487 m_recorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, m_sampleRate, m_channels,
488 m_audioFormat, m_bufferSize);
489 m_streamPlayer = new AudioTrack(AudioManager.STREAM_MUSIC, m_sampleRate, m_channels,
490 m_audioFormat, m_bufferSize, AudioTrack.MODE_STREAM);
491
492 final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
493 for (AudioDeviceInfo deviceInfo : devices) {
494 if (deviceInfo.getId() == outputId) {
495 m_streamPlayer.setPreferredDevice(deviceInfo);
496 } else if (deviceInfo.getId() == inputId) {
497 m_recorder.setPreferredDevice(deviceInfo);
498 }
499 }
500
501 m_recorder.startRecording();
502 m_isStreaming = true;
503
504 m_streamingThread = new Thread(new Runnable() {
505 @Override
506 public void run() {
507 streamSound();
508 }
509 });
510
511 m_streamingThread.start();
512 }
513
514 private static void stopSoundStreaming()
515 {
516 if (!m_isStreaming)
517 return;
518
519 m_isStreaming = false;
520 try {
521 m_streamingThread.join();
522 m_streamingThread = null;
523 } catch (InterruptedException e) {
524 e.printStackTrace();
525 }
526 m_recorder.stop();
527 m_recorder.release();
528 m_streamPlayer.release();
529 m_streamPlayer = null;
530 m_recorder = null;
531 }
532}
void AudioDeviceInfo()
[Audio output state changed]
Definition audio.cpp:168
IOBluetoothDevice * device
QPainter Context
static const QString context()
Definition java.cpp:398
QTCONCURRENT_RUN_NODISCARD auto run(QThreadPool *pool, Function &&f, Args &&...args)
EGLDeviceEXT * devices
GLenum mode
GLsizei GLenum GLenum * types
GLenum type
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
[0]
@ Handler
static int compare(quint64 a, quint64 b)
static QInputDevice::DeviceType deviceType(const UINT cursorType)
manager put(request, myData, this, [this](QRestReply &reply) { if(reply.isSuccess()) })
[5]