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 {
62 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
63 updateDeviceList();
64 }
65
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 AudioDeviceInfo getAudioInputDeviceInfo(int id)
100 {
101 final AudioDeviceInfo[] audioDevices = m_audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
102
103 for (AudioDeviceInfo deviceInfo : audioDevices) {
104 if (deviceInfo.getId() == id)
105 return deviceInfo;
106 }
107
108 return null;
109 }
110
111 @UsedFromNativeCode
112 static int getDefaultSampleRate()
113 {
114 String sampleRate = m_audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
115 return Integer.parseInt(sampleRate);
116 }
117
118 @UsedFromNativeCode
119 static boolean isBluetoothDevice(AudioDeviceInfo deviceInfo)
120 {
121 switch (deviceInfo.getType()) {
122 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
123 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
124 return true;
125 default:
126 return false;
127 }
128 }
129
130 @UsedFromNativeCode
131 static boolean prepareAudioInput(int id)
132 {
133 final AudioDeviceInfo[] audioDevices =
134 m_audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
135 for (AudioDeviceInfo deviceInfo : audioDevices) {
136 if (deviceInfo.getId() == id) {
137 switch (deviceInfo.getType())
138 {
139 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
140 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
141 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
142 case AudioDeviceInfo.TYPE_USB_HEADSET:
143 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
144 return prepareAudioDevice(deviceInfo, AudioManager.MODE_NORMAL);
145 default:
146 return true;
147 }
148 }
149 }
150 return false;
151 }
152
153 private static void setInputMuted(boolean mute)
154 {
155 // This method mutes the microphone across the entire platform
156 m_audioManager.setMicrophoneMute(mute);
157 }
158
159 private static boolean isMicrophoneMute()
160 {
161 return m_audioManager.isMicrophoneMute();
162 }
163
164 private static String audioDeviceTypeToString(int type)
165 {
166 // API <= 23 types
167 switch (type)
168 {
169 case AudioDeviceInfo.TYPE_AUX_LINE:
170 return "AUX Line";
171 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
172 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
173 return "Bluetooth";
174 case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
175 return "Built in earpiece";
176 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
177 return "Built in microphone";
178 case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
179 return "Built in speaker";
180 case AudioDeviceInfo.TYPE_DOCK:
181 return "Dock";
182 case AudioDeviceInfo.TYPE_FM:
183 return "FM";
184 case AudioDeviceInfo.TYPE_FM_TUNER:
185 return "FM TUNER";
186 case AudioDeviceInfo.TYPE_HDMI:
187 return "HDMI";
188 case AudioDeviceInfo.TYPE_HDMI_ARC:
189 return "HDMI ARC";
190 case AudioDeviceInfo.TYPE_IP:
191 return "IP";
192 case AudioDeviceInfo.TYPE_LINE_ANALOG:
193 return "Line analog";
194 case AudioDeviceInfo.TYPE_LINE_DIGITAL:
195 return "Line digital";
196 case AudioDeviceInfo.TYPE_TV_TUNER:
197 return "TV tuner";
198 case AudioDeviceInfo.TYPE_USB_ACCESSORY:
199 return "USB accessory";
200 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
201 return "Wired headphones";
202 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
203 return "Wired headset";
204 }
205
206 // API 24
207 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
208 if (type == AudioDeviceInfo.TYPE_BUS)
209 return "Bus";
210 }
211
212 return "Unknown-Type";
213
214 }
215
216 private static final HashMap<Integer, Integer> priorityMap = new HashMap<Integer, Integer>() {{
217 put(AudioDeviceInfo.TYPE_WIRED_HEADSET, 1);
218 put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, 1);
219 put(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 2);
220 put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, 2);
221 put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, 3);
222 }};
223 private static final int DEFAULT_PRIORITY = 4;
224
225 private static void sortAudioDevices(AudioDeviceInfo[] devices) {
226 Comparator<AudioDeviceInfo> deviceTypeComparator = new Comparator<AudioDeviceInfo>() {
227 @Override
228 public int compare(AudioDeviceInfo device1, AudioDeviceInfo device2) {
229 return getPriority(device1) - getPriority(device2);
230 }
231
232 private int getPriority(AudioDeviceInfo device) {
233 return priorityMap.getOrDefault(device.getType(), DEFAULT_PRIORITY);
234 }
235 };
236
237 Arrays.sort(devices, deviceTypeComparator);
238 }
239
240 private static AudioDeviceInfo[] getAudioDevices(int type)
241 {
242 ArrayList<AudioDeviceInfo> filteredDevices = new ArrayList<>();
243
244 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
245 boolean builtInMicAdded = false;
246 boolean bluetoothDeviceAdded = false;
247 AudioDeviceInfo[] audioDevices = m_audioManager.getDevices(type);
248 sortAudioDevices(audioDevices);
249
250 for (AudioDeviceInfo deviceInfo : audioDevices) {
251 String deviceType = audioDeviceTypeToString(deviceInfo.getType());
252
253 if (deviceType.equals(audioDeviceTypeToString(AudioDeviceInfo.TYPE_UNKNOWN))) {
254 // Not supported device type
255 continue;
256 } else if (deviceType.equals(audioDeviceTypeToString(AudioDeviceInfo.TYPE_BUILTIN_MIC))) {
257 if (builtInMicAdded) {
258 // Built in mic already added. Second built in mic is CAMCORDER, but there
259 // is no reliable way of selecting it. AudioSource.MIC usually means the
260 // primary mic. AudioSource.CAMCORDER source might mean the secondary mic,
261 // but there's no guarantee. It depends e.g. on the physical placement
262 // of the mics. That's why we will not add built in microphone twice.
263 // Should we?
264 continue;
265 }
266 builtInMicAdded = true;
267 } else if (isBluetoothDevice(deviceInfo)) {
268 if (bluetoothDeviceAdded) {
269 // Bluetooth device already added. Second device is just a different
270 // technology profille (like A2DP or SCO). We should not add the same
271 // device twice. Should we?
272 continue;
273 }
274 bluetoothDeviceAdded = true;
275 }
276
277 filteredDevices.add(deviceInfo);
278 }
279 }
280
281 return filteredDevices.toArray(new AudioDeviceInfo[filteredDevices.size()]);
282 }
283
284 final private static int [] bluetoothTypes = {
285 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
286 AudioDeviceInfo.TYPE_BLUETOOTH_SCO
287 };
288 final private static int [] wiredTypes = {
289 AudioDeviceInfo.TYPE_WIRED_HEADSET,
290 AudioDeviceInfo.TYPE_WIRED_HEADPHONES
291 };
292
293 private static boolean containsAnyOfType(AudioDeviceInfo[] devices, int[]... types) {
294
295 return Arrays.stream(devices)
296 .anyMatch(device -> Arrays.stream(types)
297 .flatMapToInt(Arrays::stream)
298 .anyMatch(type -> device.getType() == type));
299 }
300
301 private static int getCorrectModeIfContainsAnyOfType(AudioDeviceInfo[] devices, int[]... types) {
302 return containsAnyOfType(devices, types) ?
303 AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL;
304 }
305
306 private static int getModeForWired(AudioDeviceInfo[] audioDevices)
307 {
308 return getCorrectModeIfContainsAnyOfType(audioDevices, bluetoothTypes);
309 }
310
311 private static int getModeForBluetooth(AudioDeviceInfo[] audioDevices)
312 {
313 return getCorrectModeIfContainsAnyOfType(audioDevices, wiredTypes);
314 }
315
316 private static int getModeForSpeaker(AudioDeviceInfo[] audioDevices)
317 {
318 return getCorrectModeIfContainsAnyOfType(audioDevices, bluetoothTypes, wiredTypes);
319 }
320
321 private static boolean prepareAudioOutput(int id)
322 {
323 final AudioDeviceInfo[] audioDevices =
324 m_audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
325 for (AudioDeviceInfo deviceInfo : audioDevices) {
326 if (deviceInfo.getId() == id) {
327 switch (deviceInfo.getType())
328 {
329 case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
330 case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
331 return prepareAudioDevice(deviceInfo, getModeForBluetooth(audioDevices));
332 case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
333 return prepareAudioDevice(deviceInfo, getModeForSpeaker(audioDevices));
334 case AudioDeviceInfo.TYPE_WIRED_HEADSET:
335 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
336 return prepareAudioDevice(deviceInfo, getModeForWired(audioDevices));
337 case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
338 // It doesn't work when WIRED HEADPHONES are connected
339 // Earpiece has the lowest priority and setWiredHeadsetOn(boolean)
340 // method to force it is deprecated
341 Log.w(TAG, "Built in Earpiece may not work when "
342 + "Wired Headphones are connected");
343 return prepareAudioDevice(deviceInfo, AudioManager.MODE_IN_CALL);
344 case AudioDeviceInfo.TYPE_HDMI:
345 case AudioDeviceInfo.TYPE_HDMI_ARC:
346 case AudioDeviceInfo.TYPE_HDMI_EARC:
347 return prepareAudioDevice(deviceInfo, AudioManager.MODE_NORMAL);
348 default:
349 return false;
350 }
351 }
352 }
353 return false;
354 }
355
360 private static AudioDeviceInfo getValidCommunicationDevice(AudioDeviceInfo device) {
361 if (device.isSink())
362 return device;
363
364 if (isBluetoothDevice(device)) {
365 // For Bluetooth sources, get output device with the same type and address
366 List<AudioDeviceInfo> communicationDevices = m_audioManager.getAvailableCommunicationDevices();
367 for (AudioDeviceInfo communicationDevice : communicationDevices) {
368 boolean isSameType = communicationDevice.getType() == device.getType();
369 boolean isSameAddress = communicationDevice.getAddress().equals(device.getAddress());
370 if (isSameType && isSameAddress)
371 return communicationDevice;
372 }
373
374 Log.w(TAG, "No matching bluetooth output device found for " + device);
375 }
376
377 return null;
378 }
379
380 private static boolean deviceIsCurrentCommunicationDevice(AudioDeviceInfo device) {
381 if (m_currentCommunicationDeviceId == -1 || device == null) {
382 return false;
383 }
384
385 return m_currentCommunicationDeviceId == device.getId();
386 }
387
388 @UsedFromNativeCode
389 static void releaseAudioDevice(int id) {
390 final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
392 if (device.getId() == id)
393 releaseAudioDevice(device);
394 }
395 }
396
400 private static void releaseAudioDevice(AudioDeviceInfo deviceInfo) {
401 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
402 // If device was used as the communication device, it should be cleared
403 AudioDeviceInfo communicationDevice = getValidCommunicationDevice(deviceInfo);
404 if (!deviceIsCurrentCommunicationDevice(communicationDevice))
405 return;
406
407 if (m_communicationDeviceCounter.decrementAndGet() == 0) {
408 m_audioManager.clearCommunicationDevice();
409 m_currentCommunicationDeviceId = -1;
410 }
411 } else if (isBluetoothDevice(deviceInfo) && m_audioManager.isBluetoothScoOn()
412 && m_scoCounter.decrementAndGet() == 0) {
413 m_audioManager.stopBluetoothSco();
414 m_audioManager.setBluetoothScoOn(false);
415 } else if (deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
416 && m_speakerphoneCounter.decrementAndGet() == 0) {
417 m_audioManager.setSpeakerphoneOn(false);
418 }
419 }
420
421 private static boolean prepareAudioDevice(AudioDeviceInfo deviceInfo, int mode)
422 {
423 if (deviceInfo == null)
424 return false;
425
426 m_audioManager.setMode(mode);
427
428 boolean isSink = deviceInfo.isSink();
429 if (isSink)
430 m_currentOutputId = deviceInfo.getId();
431
432 boolean isBluetoothDevice = isBluetoothDevice(deviceInfo);
433 boolean isBluetoothSource = isBluetoothDevice && !isSink;
434 boolean isCommunicationMode = (mode == AudioManager.MODE_IN_CALL
435 || mode == AudioManager.MODE_IN_COMMUNICATION);
436
437 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
438 if (!isBluetoothSource && !isCommunicationMode)
439 return true;
440
441 // For communication modes and Bluetooth sources, it's required to set a communication device
442 AudioDeviceInfo communicationDevice = getValidCommunicationDevice(deviceInfo);
443 if (communicationDevice == null) {
444 Log.w(TAG, "No suitable communication device to set to enable communication via "
445 + deviceInfo.getId());
446 return false;
447 }
448
449 if (deviceIsCurrentCommunicationDevice(communicationDevice)) {
450 m_communicationDeviceCounter.incrementAndGet();
451 return true;
452 } else if (m_audioManager.setCommunicationDevice(communicationDevice)) {
453 // NOTE: Keep track of communication devices we set, as it takes time for it to be
454 // fully operational.
455 // TODO: Other applications can set a different communication device, in which case
456 // we should probably register a listener and clear our tracking when the
457 // communication device unexpectedly changes
458 m_currentCommunicationDeviceId = communicationDevice.getId();
459 m_communicationDeviceCounter.set(1);
460 return true;
461 }
462
463 return false;
464 }
465
466 boolean isSpeakerphoneDevice = deviceInfo.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
467
468 if (isBluetoothSource && m_scoCounter.getAndIncrement() == 0) {
469 m_audioManager.startBluetoothSco();
470 m_audioManager.setBluetoothScoOn(true);
471 } else if (isSpeakerphoneDevice
472 && m_speakerphoneCounter.getAndIncrement() == 0) {
473 // TODO: Check if setting speakerphone is actually required for anything, it's not
474 // recommended in Android docs.
475 m_audioManager.setSpeakerphoneOn(true);
476 }
477
478 return true;
479 }
480
481 private static void streamSound()
482 {
483 byte data[] = new byte[m_bufferSize];
484 while (m_isStreaming) {
485 m_recorder.read(data, 0, m_bufferSize);
486 m_streamPlayer.play();
487 m_streamPlayer.write(data, 0, m_bufferSize);
488 m_streamPlayer.stop();
489 }
490 }
491
492 private static void startSoundStreaming(int inputId, int outputId)
493 {
494 if (m_isStreaming)
495 stopSoundStreaming();
496
497 m_recorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, m_sampleRate, m_channels,
498 m_audioFormat, m_bufferSize);
499 m_streamPlayer = new AudioTrack(AudioManager.STREAM_MUSIC, m_sampleRate, m_channels,
500 m_audioFormat, m_bufferSize, AudioTrack.MODE_STREAM);
501
502 final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
503 for (AudioDeviceInfo deviceInfo : devices) {
504 if (deviceInfo.getId() == outputId) {
505 m_streamPlayer.setPreferredDevice(deviceInfo);
506 } else if (deviceInfo.getId() == inputId) {
507 m_recorder.setPreferredDevice(deviceInfo);
508 }
509 }
510
511 m_recorder.startRecording();
512 m_isStreaming = true;
513
514 m_streamingThread = new Thread(new Runnable() {
515 @Override
516 public void run() {
517 streamSound();
518 }
519 });
520
521 m_streamingThread.start();
522 }
523
524 private static void stopSoundStreaming()
525 {
526 if (!m_isStreaming)
527 return;
528
529 m_isStreaming = false;
530 try {
531 m_streamingThread.join();
532 m_streamingThread = null;
533 } catch (InterruptedException e) {
534 e.printStackTrace();
535 }
536 m_recorder.stop();
537 m_recorder.release();
538 m_streamPlayer.release();
539 m_streamPlayer = null;
540 m_recorder = null;
541 }
542}
void AudioDeviceInfo()
[Audio callback capture setup peak meter]
Definition audio.cpp:278
IOBluetoothDevice * device
QPainter Context
static const QString context()
Definition java.cpp:396
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]