Skip to content

Commit d68fa16

Browse files
invictus737s1lviu
authored andcommitted
feat: Add secure audio cooperation and microphone gain control
Two major enhancements for improved iOS audio handling: ## 🔒 Secure Audio Cooperation - Prevent audio leakage from other apps during PTT transmission - Dynamic audio session management: ducking during PTT, mixing when idle - Enhanced security validation in audio input processing - Smooth transitions between cooperative and exclusive audio modes ### Changes: - IOSAudioManager: Added ducking/mixing session configuration methods - ReflectorClient: Secure PTT flow with session mode switching - AudioEngine: Security validation to prevent non-microphone audio transmission ## 🎚️ Microphone Gain Control (-20dB to +20dB) - Real-time adjustable microphone gain slider in UI - Integrates with existing iOS 24dB base gain processing - Professional iOS-style slider with live dB value display - Thread-safe gain updates between UI and audio engine ### Changes: - ReflectorClient: Added micGainDb Q_PROPERTY with bounds checking - AudioEngine: Configurable gain application in input processing - Main.qml: Native iOS slider control positioned above connect button ### Technical Details: - Security: Only microphone input transmitted over radio network - Cooperation: Music/podcast apps resume after PTT release - Flexibility: -20dB to +20dB range with 0.5dB precision - Safety: Automatic bounds checking and soft limiting
1 parent 2d0cff6 commit d68fa16

File tree

7 files changed

+283
-22
lines changed

7 files changed

+283
-22
lines changed

iOS/AudioEngine.cpp

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,16 @@ void AudioEngine::onAudioInputReadyRead()
771771
return;
772772
}
773773

774+
#if defined(Q_OS_IOS)
775+
// SECURITY CHECK: Verify audio session is in secure mode during PTT
776+
// This prevents other apps' audio from being transmitted over the radio network
777+
if (!ios_isAudioSessionSecure()) {
778+
qWarning() << "AudioEngine: SECURITY WARNING - Audio session not in secure mode during PTT, dropping audio data";
779+
qWarning() << "AudioEngine: This prevents other apps' audio from being transmitted over radio";
780+
return;
781+
}
782+
#endif
783+
774784
QByteArray pcmData = m_audioInputDevice->readAll();
775785
if (pcmData.isEmpty()) {
776786
qDebug() << "AudioEngine::onAudioInputReadyRead - No data available";
@@ -820,9 +830,11 @@ void AudioEngine::onAudioInputReadyRead()
820830

821831
#if defined(Q_OS_IOS)
822832
// iOS-specific input gain boost to compensate for low microphone levels
823-
// Apply aggressive gain to match Android -6dB levels (iOS typically -30dB without boost)
824-
// Target: 24dB boost (16x amplification) to go from -30dB to -6dB
825-
const float iosInputGain = 16.0f;
833+
// Apply base gain (24dB/16x) plus user-configurable mic gain
834+
// Base: 24dB boost (16x amplification) to go from -30dB to -6dB
835+
// User: Additional -20dB to +20dB adjustment
836+
const float iosBaseGain = 16.0f;
837+
const float totalGain = iosBaseGain * m_micGainLinear;
826838

827839
// Debug: Log input levels periodically for Int16 format
828840
static int debugCounterInt16 = 0;
@@ -831,15 +843,15 @@ void AudioEngine::onAudioInputReadyRead()
831843
for (int i = 0; i < monoSamples; ++i) {
832844
maxLevel = std::max(maxLevel, std::abs(m_reusableFloatBuffer[i]));
833845
}
834-
qDebug() << "iOS Int16 input level before gain:" << maxLevel << "after gain:" << (maxLevel * iosInputGain);
835-
qDebug() << "iOS Int16 gain applied: 16x (" << (20 * log10(iosInputGain)) << "dB)";
846+
qDebug() << "iOS Int16 input level before gain:" << maxLevel << "after gain:" << (maxLevel * totalGain);
847+
qDebug() << "iOS Int16 total gain applied:" << totalGain << "x (Base: 24dB + Mic:" << m_micGainDb << "dB)";
836848
if (maxLevel < 0.001f) {
837849
qWarning() << "iOS WARNING: Input level extremely low!" << maxLevel << "- Check microphone permissions";
838850
}
839851
}
840852

841853
for (int i = 0; i < monoSamples; ++i) {
842-
float sample = m_reusableFloatBuffer[i] * iosInputGain;
854+
float sample = m_reusableFloatBuffer[i] * totalGain;
843855
// Professional soft limiting for high-gain scenarios
844856
// Use tanh() for smooth saturation instead of hard clipping
845857
if (std::abs(sample) > 0.9f) {
@@ -881,9 +893,11 @@ void AudioEngine::onAudioInputReadyRead()
881893

882894
#if defined(Q_OS_IOS)
883895
// iOS-specific input gain boost to compensate for low microphone levels
884-
// Apply aggressive gain to match Android -6dB levels (iOS typically -30dB without boost)
885-
// Target: 24dB boost (16x amplification) to go from -30dB to -6dB
886-
const float iosInputGain = 16.0f;
896+
// Apply base gain (24dB/16x) plus user-configurable mic gain
897+
// Base: 24dB boost (16x amplification) to go from -30dB to -6dB
898+
// User: Additional -20dB to +20dB adjustment
899+
const float iosBaseGain = 16.0f;
900+
const float totalGain = iosBaseGain * m_micGainLinear;
887901

888902
// Debug: Log input levels periodically for Float format
889903
static int debugCounterFloat = 0;
@@ -892,15 +906,15 @@ void AudioEngine::onAudioInputReadyRead()
892906
for (int i = 0; i < monoSamples; ++i) {
893907
maxLevel = std::max(maxLevel, std::abs(m_reusableFloatBuffer[i]));
894908
}
895-
qDebug() << "iOS Float input level before gain:" << maxLevel << "after gain:" << (maxLevel * iosInputGain);
896-
qDebug() << "iOS Float gain applied: 16x (" << (20 * log10(iosInputGain)) << "dB)";
909+
qDebug() << "iOS Float input level before gain:" << maxLevel << "after gain:" << (maxLevel * totalGain);
910+
qDebug() << "iOS Float total gain applied:" << totalGain << "x (Base: 24dB + Mic:" << m_micGainDb << "dB)";
897911
if (maxLevel < 0.001f) {
898912
qWarning() << "iOS WARNING: Input level extremely low!" << maxLevel << "- Check microphone permissions";
899913
}
900914
}
901915

902916
for (int i = 0; i < monoSamples; ++i) {
903-
float sample = m_reusableFloatBuffer[i] * iosInputGain;
917+
float sample = m_reusableFloatBuffer[i] * totalGain;
904918
// Professional soft limiting for high-gain scenarios
905919
// Use tanh() for smooth saturation instead of hard clipping
906920
if (std::abs(sample) > 0.9f) {
@@ -1320,4 +1334,19 @@ void AudioEngine::AudioLimiter::processAudio(float* samples, int count) {
13201334
// Apply gain reduction to input sample
13211335
samples[i] = outputGain_ * samples[i] * gainReduction;
13221336
}
1337+
}
1338+
1339+
void AudioEngine::setMicGainDb(double gainDb)
1340+
{
1341+
// Clamp gain to -20dB to +20dB range for safety
1342+
gainDb = qBound(-20.0, gainDb, 20.0);
1343+
1344+
if (qAbs(m_micGainDb - gainDb) < 0.1) return; // Avoid unnecessary updates
1345+
1346+
m_micGainDb = gainDb;
1347+
1348+
// Convert dB to linear gain factor: gain_linear = 10^(dB/20)
1349+
m_micGainLinear = static_cast<float>(std::pow(10.0, gainDb / 20.0));
1350+
1351+
qDebug() << "AudioEngine: Microphone gain updated to" << gainDb << "dB (linear:" << m_micGainLinear << ")";
13231352
}

iOS/AudioEngine.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public slots:
6767
void onActivityPaused();
6868
void onActivityResumed();
6969
void allSamplesFlushed();
70+
void setMicGainDb(double gainDb);
7071

7172
signals:
7273
void audioReadyChanged(bool ready);
@@ -117,6 +118,10 @@ private slots:
117118
bool m_audioFocusPaused = false;
118119
QDateTime m_lastAudioWrite;
119120

121+
// Microphone gain control (-20dB to +20dB)
122+
double m_micGainDb = 0.0;
123+
float m_micGainLinear = 1.0f;
124+
120125
// Pre-allocated buffers for performance optimization
121126
std::vector<float> m_reusableFloatBuffer;
122127
std::vector<unsigned char> m_reusableOpusBuffer;

iOS/Main.qml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,111 @@ Item {
318318
}
319319
}
320320

321+
// Microphone Gain Control
322+
ColumnLayout {
323+
Layout.fillWidth: true
324+
Layout.topMargin: 6
325+
Layout.bottomMargin: 3
326+
327+
RowLayout {
328+
Layout.fillWidth: true
329+
330+
Label {
331+
text: "Mic Gain:"
332+
color: isDarkMode ? "#FFFFFF" : "#000000"
333+
font.pixelSize: Qt.platform.os === "ios" ? 16 : 14
334+
}
335+
336+
Item { Layout.fillWidth: true } // Spacer
337+
338+
Label {
339+
text: ReflectorClient.micGainDb.toFixed(1) + " dB"
340+
color: isDarkMode ? "#8E8E93" : "#666666"
341+
font.pixelSize: Qt.platform.os === "ios" ? 14 : 12
342+
font.family: Qt.platform.os === "ios" ? "SF Pro Text" : "monospace"
343+
}
344+
}
345+
346+
Slider {
347+
id: micGainSlider
348+
Layout.fillWidth: true
349+
Layout.preferredHeight: Qt.platform.os === "ios" ? 30 : implicitHeight
350+
from: -20.0
351+
to: 20.0
352+
stepSize: 0.5
353+
value: ReflectorClient.micGainDb
354+
355+
onValueChanged: {
356+
if (Math.abs(value - ReflectorClient.micGainDb) > 0.1) {
357+
ReflectorClient.micGainDb = value
358+
}
359+
}
360+
361+
background: Rectangle {
362+
x: micGainSlider.leftPadding
363+
y: micGainSlider.topPadding + micGainSlider.availableHeight / 2 - height / 2
364+
width: micGainSlider.availableWidth
365+
height: Qt.platform.os === "ios" ? 4 : 6
366+
radius: 2
367+
color: isDarkMode ? "#38383A" : "#C6C6C8"
368+
369+
Rectangle {
370+
width: micGainSlider.visualPosition * parent.width
371+
height: parent.height
372+
color: Qt.platform.os === "ios" ? "#007AFF" : (isDarkMode ? "#4A90E2" : "#0078D4")
373+
radius: 2
374+
}
375+
}
376+
377+
handle: Rectangle {
378+
x: micGainSlider.leftPadding + micGainSlider.visualPosition * (micGainSlider.availableWidth - width)
379+
y: micGainSlider.topPadding + micGainSlider.availableHeight / 2 - height / 2
380+
width: Qt.platform.os === "ios" ? 28 : 20
381+
height: Qt.platform.os === "ios" ? 28 : 20
382+
radius: Qt.platform.os === "ios" ? 14 : 10
383+
color: Qt.platform.os === "ios" ? "#FFFFFF" : (isDarkMode ? "#FFFFFF" : "#FFFFFF")
384+
border.color: Qt.platform.os === "ios" ? "#C6C6C8" : (isDarkMode ? "#666666" : "#C6C6C8")
385+
border.width: Qt.platform.os === "ios" ? 1 : 1
386+
387+
// iOS-style shadow
388+
layer.enabled: Qt.platform.os === "ios"
389+
layer.effect: DropShadow {
390+
horizontalOffset: 0
391+
verticalOffset: 1
392+
radius: 3
393+
samples: 7
394+
color: "#40000000"
395+
}
396+
}
397+
}
398+
399+
RowLayout {
400+
Layout.fillWidth: true
401+
402+
Label {
403+
text: "-20"
404+
color: isDarkMode ? "#8E8E93" : "#999999"
405+
font.pixelSize: Qt.platform.os === "ios" ? 12 : 10
406+
}
407+
408+
Item { Layout.fillWidth: true } // Spacer
409+
410+
Label {
411+
text: "0"
412+
color: isDarkMode ? "#8E8E93" : "#999999"
413+
font.pixelSize: Qt.platform.os === "ios" ? 12 : 10
414+
}
415+
416+
Item { Layout.fillWidth: true } // Spacer
417+
418+
Label {
419+
text: "+20"
420+
color: isDarkMode ? "#8E8E93" : "#999999"
421+
font.pixelSize: Qt.platform.os === "ios" ? 12 : 10
422+
}
423+
}
424+
}
425+
321426
Button {
322427
id: connectButton
323428
Layout.fillWidth: true

iOS/ReflectorClient.cpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ void ReflectorClient::initializeAudioEngine()
251251
m_audioEngine = new AudioEngine();
252252
m_audioEngine->moveToThread(m_audioThread);
253253

254+
// Initialize AudioEngine with current mic gain setting
255+
QMetaObject::invokeMethod(m_audioEngine, "setMicGainDb", Qt::QueuedConnection,
256+
Q_ARG(double, m_micGainDb));
257+
254258
// Connect signals from AudioEngine to ReflectorClient
255259
connect(m_audioEngine, &AudioEngine::audioReadyChanged, this, [this](bool ready) {
256260
m_audioReady = ready;
@@ -350,6 +354,24 @@ void ReflectorClient::sendHeartbeat()
350354
QString ReflectorClient::connectionStatus() const { return m_connectionStatus; }
351355
bool ReflectorClient::pttActive() const { return m_pttActive; }
352356
QString ReflectorClient::currentTalker() const { return m_currentTalker; }
357+
358+
void ReflectorClient::setMicGainDb(double gainDb) {
359+
// Clamp gain to -20dB to +20dB range for safety
360+
gainDb = qBound(-20.0, gainDb, 20.0);
361+
362+
if (qAbs(m_micGainDb - gainDb) < 0.1) return; // Avoid unnecessary updates
363+
364+
m_micGainDb = gainDb;
365+
emit micGainDbChanged();
366+
367+
// Notify AudioEngine about the gain change if connected
368+
if (m_audioEngine) {
369+
QMetaObject::invokeMethod(m_audioEngine, "setMicGainDb", Qt::QueuedConnection,
370+
Q_ARG(double, gainDb));
371+
}
372+
373+
qDebug() << "Microphone gain set to:" << gainDb << "dB";
374+
}
353375
void ReflectorClient::connectToServer(const QString &host, int port, const QString &authKey, const QString &callsign, quint32 talkgroup) {
354376
if (m_state != Disconnected) return;
355377

@@ -493,6 +515,11 @@ void ReflectorClient::startTransmission() {
493515

494516
#if defined(Q_OS_IOS)
495517
// iOS microphone permission is handled by the system via Info.plist
518+
// SECURITY: Configure audio session to DUCK other apps before PTT
519+
// This prevents other audio from mixing into microphone input
520+
ios_configureDuckingAudioSession();
521+
qDebug() << "iOS: Audio session configured for secure PTT (ducking mode)";
522+
496523
// Request audio focus equivalent to Android audio focus
497524
IOSVoIPHandler::instance()->requestAudioFocus();
498525
qDebug() << "iOS audio focus requested for PTT";
@@ -518,7 +545,7 @@ void ReflectorClient::startTransmission() {
518545

519546
// Start recording through AudioEngine
520547
QMetaObject::invokeMethod(m_audioEngine, "startRecording", Qt::QueuedConnection);
521-
qInfo() << "PTT Pressed: Recording started.";
548+
qInfo() << "PTT Pressed: Recording started (secure mode - other apps ducked).";
522549
}
523550
void ReflectorClient::pttReleased() {
524551
if (!m_pttActive) return;
@@ -556,6 +583,13 @@ void ReflectorClient::pttReleased() {
556583
QMetaObject::invokeMethod(m_audioEngine, "stopRecording", Qt::QueuedConnection);
557584
}
558585

586+
#if defined(Q_OS_IOS)
587+
// RESTORE: Configure audio session to MIX with other apps after PTT
588+
// This allows cooperative behavior with music, podcasts, etc.
589+
ios_configureMixingAudioSession();
590+
qDebug() << "iOS: Audio session restored to cooperative mixing mode after PTT";
591+
#endif
592+
559593
// Inform the server that we are done transmitting so talker state can be
560594
// updated immediately instead of waiting for a timeout.
561595
QByteArray flush(sizeof(Svxlink::UdpMsgHeader), Qt::Uninitialized);

iOS/ReflectorClient.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ReflectorClient : public QObject
4949
Q_PROPERTY(bool isDisconnected READ isDisconnected NOTIFY connectionStatusChanged)
5050
Q_PROPERTY(bool audioReady READ audioReady NOTIFY audioReadyChanged)
5151
Q_PROPERTY(bool isReceivingAudio READ isReceivingAudio NOTIFY isReceivingAudioChanged)
52+
Q_PROPERTY(double micGainDb READ micGainDb WRITE setMicGainDb NOTIFY micGainDbChanged)
5253

5354
public:
5455
static ReflectorClient* instance();
@@ -62,6 +63,8 @@ class ReflectorClient : public QObject
6263
bool isDisconnected() const { return m_state == Disconnected; }
6364
bool audioReady() const { return m_audioReady; }
6465
bool isReceivingAudio() const { return m_isReceivingAudio; }
66+
double micGainDb() const { return m_micGainDb; }
67+
void setMicGainDb(double gainDb);
6568

6669
Q_INVOKABLE void connectToServer(const QString &host, int port, const QString &authKey, const QString &callsign, quint32 talkgroup);
6770
Q_INVOKABLE void disconnectFromServer();
@@ -95,6 +98,7 @@ public slots:
9598
void txTimeStringChanged();
9699
void audioReadyChanged();
97100
void isReceivingAudioChanged();
101+
void micGainDbChanged();
98102

99103
// New protocol signals
100104
void connectedNodesChanged(const QStringList &nodes);
@@ -174,6 +178,7 @@ private slots:
174178
QString m_connectionStatus = "Disconnected";
175179
bool m_pttActive = false;
176180
bool m_audioReady = false;
181+
double m_micGainDb = 0.0; // Default 0dB gain (no change)
177182

178183
QTcpSocket* m_tcpSocket = nullptr;
179184
QUdpSocket* m_udpSocket = nullptr;

iOS/ios/IOSAudioManager.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ void ios_acquireScreenWakeLock(void);
4444
void ios_releaseScreenWakeLock(void);
4545
int ios_isScreenWakeLockActive(void);
4646

47+
// Audio session cooperation and security functions
48+
void ios_configureDuckingAudioSession(void);
49+
void ios_configureMixingAudioSession(void);
50+
int ios_isAudioSessionSecure(void);
51+
4752
#ifdef __cplusplus
4853
}
4954
#endif
@@ -66,6 +71,11 @@ int ios_isScreenWakeLockActive(void);
6671
- (void)releaseScreenWakeLock;
6772
- (BOOL)isScreenWakeLockActive;
6873

74+
// Audio session cooperation and security methods
75+
- (void)configureDuckingAudioSession;
76+
- (void)configureMixingAudioSession;
77+
- (BOOL)isAudioSessionSecure;
78+
6979
@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTask;
7080
@property (nonatomic, strong) AVAudioSession *audioSession;
7181
@property (nonatomic, assign) BOOL screenWakeLockActive;

0 commit comments

Comments
 (0)