From ba0c318beffc88f240a064eed346f56d34f6ace3 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Thu, 28 May 2026 13:48:02 +0200 Subject: [PATCH 1/4] Optimize headset audio This cleans up the call flow audio, inserts some more silence and delays sending of certain teams display HID commands that led to a cut-off for end-call and congestion tones. --- src/media/AudioManager.cpp | 34 +++++++++++++++ src/media/AudioManager.h | 5 +++ src/media/AudioPort.cpp | 78 ++++++++++++++++++++++++++++++---- src/media/AudioPort.h | 12 +++++- src/sip/RingTone.cpp | 28 +++++++++--- src/sip/RingTone.h | 2 + src/sip/SIPAccount.cpp | 2 +- src/sip/SIPCall.cpp | 19 ++++++--- src/usb/HeadsetDeviceProxy.cpp | 31 +++++++++++--- src/usb/HeadsetDeviceProxy.h | 2 + 10 files changed, 181 insertions(+), 32 deletions(-) diff --git a/src/media/AudioManager.cpp b/src/media/AudioManager.cpp index 7fb01118..78d661b2 100644 --- a/src/media/AudioManager.cpp +++ b/src/media/AudioManager.cpp @@ -528,3 +528,37 @@ void AudioManager::setPlaybackAudioVolume(qreal volume) } } } + +void AudioManager::acquireDevice() +{ + if (++m_acquireRefCount > 1) { + return; + } + + if (m_playbackAudioPort) { + m_playbackAudioPort->acquire(); + } + + if (m_captureAudioPort) { + m_captureAudioPort->acquire(); + } +} + +void AudioManager::releaseDevice() +{ + if (m_acquireRefCount == 0) { + return; + } + + if (--m_acquireRefCount > 0) { + return; + } + + if (m_playbackAudioPort) { + m_playbackAudioPort->release(); + } + + if (m_captureAudioPort) { + m_captureAudioPort->release(); + } +} diff --git a/src/media/AudioManager.h b/src/media/AudioManager.h index 921d082a..e65136ef 100644 --- a/src/media/AudioManager.h +++ b/src/media/AudioManager.h @@ -67,6 +67,9 @@ class AudioManager : public QObject void *userdata); #endif + void acquireDevice(); + void releaseDevice(); + pj::AudioMedia &getPlaybackDevMedia() const; pj::AudioMedia &getCaptureDevMedia() const; @@ -139,6 +142,8 @@ private Q_SLOTS: unsigned m_captureDeviceId = 0; unsigned m_currentAudioProfile = 0; + int m_acquireRefCount = 0; + QTimer m_updateDebouncer; QList m_devices; diff --git a/src/media/AudioPort.cpp b/src/media/AudioPort.cpp index e3e0049f..8cfa2ed8 100644 --- a/src/media/AudioPort.cpp +++ b/src/media/AudioPort.cpp @@ -13,7 +13,7 @@ using namespace std::chrono_literals; AudioPort::AudioPort(QAudioDevice device) : m_device(device) { - m_idleTimer.setInterval(1s); + m_idleTimer.setInterval(10s); connect(&m_idleTimer, &QTimer::timeout, this, &AudioPort::stopIO); connect(this, &AudioPort::startIdleTimer, this, @@ -168,6 +168,11 @@ void AudioPort::startSinkIO() { m_idleTimer.stop(); + if (m_isDraining && !m_sink.isNull()) { + m_isDraining = false; + return; + } + if (!m_sink.isNull()) { stopSinkIO(); } @@ -191,17 +196,25 @@ void AudioPort::startSinkIO() void AudioPort::stopSinkIO() { m_idleTimer.stop(); + if (m_sink.isNull() || m_isDraining) { + return; + } - if (m_sink) { - writeSilenceMS(SILENCE_BUFFER_MS); + writeSilenceMS(SILENCE_BUFFER_MS); - m_sink->stop(); - m_sink->deleteLater(); - m_sink = nullptr; - m_io = nullptr; - } + m_isDraining = true; + QTimer::singleShot(SILENCE_BUFFER_MS + 200, this, [this]() { + m_isDraining = false; - Q_EMIT audioSinkChanged(); + if (m_sink) { + m_sink->stop(); + m_sink->deleteLater(); + m_sink = nullptr; + m_io = nullptr; + } + + Q_EMIT audioSinkChanged(); + }); } void AudioPort::startSourceIO() @@ -267,6 +280,11 @@ void AudioPort::onFrameRequested(pj::MediaFrame &frame) return; } + if (m_isWarmingUp) { + m_isWarmingUp = false; + QObject::disconnect(m_warmUpDrain); + } + auto bytes = m_io->read(frame.size); if (!m_isMuted) { @@ -297,8 +315,50 @@ void AudioPort::onFrameReceived(pj::MediaFrame &frame) return; } + m_isWarmingUp = false; + m_io->write(reinterpret_cast(frame.buf.data()), frame.size); // Auto destroy sink after timeout Q_EMIT startIdleTimer(); } + +void AudioPort::acquire() +{ + if (m_device.mode() == QAudioDevice::Mode::Input) { + if (m_source.isNull()) { + startSourceIO(); + } + } else { + if (m_sink.isNull()) { + startSinkIO(); + } + } + + m_isWarmingUp = true; + + if (m_device.mode() == QAudioDevice::Mode::Input && !m_io.isNull()) { + QObject::disconnect(m_warmUpDrain); + + m_warmUpDrain = connect(m_io.data(), &QIODevice::readyRead, this, [this]() { + if (m_isWarmingUp && !m_io.isNull()) { + m_io->readAll(); + } + }); + } +} + +void AudioPort::release() +{ + if (!m_isWarmingUp) { + return; + } + + m_isWarmingUp = false; + + QObject::disconnect(m_warmUpDrain); + + if (!m_idleTimer.isActive()) { + stopIO(); + } +} diff --git a/src/media/AudioPort.h b/src/media/AudioPort.h index 270dfb87..dd02b945 100644 --- a/src/media/AudioPort.h +++ b/src/media/AudioPort.h @@ -24,6 +24,9 @@ class AudioPort : public QObject, public pj::AudioMediaPort QString getDeviceID() const; QString getSystemDeviceID() const; + void acquire(); + void release(); + void setAudioDevice(QAudioDevice device); QAudioDevice audioDevice() { return m_device; } @@ -32,6 +35,8 @@ class AudioPort : public QObject, public pj::AudioMediaPort qreal sourceLevel() const { return m_sourceAudioLevel; } + void writeSilenceMS(unsigned milliseconds); + Q_SIGNALS: void startIdleTimer(); void audioSourceChanged(); @@ -39,8 +44,6 @@ class AudioPort : public QObject, public pj::AudioMediaPort void sourceLevelChanged(qreal level); private: - void writeSilenceMS(unsigned milliseconds); - void startIO(); void stopIO(); @@ -55,6 +58,9 @@ class AudioPort : public QObject, public pj::AudioMediaPort void setSourceAudioLevel(qreal level); bool m_isMuted = false; + bool m_isDraining = false; + bool m_isWarmingUp = false; + QAudioDevice m_device; QPointer m_io; @@ -67,4 +73,6 @@ class AudioPort : public QObject, public pj::AudioMediaPort pj::MediaFormatAudio m_pj_fmt; QAudioFormat m_audioFormat; + + QMetaObject::Connection m_warmUpDrain; }; diff --git a/src/sip/RingTone.cpp b/src/sip/RingTone.cpp index 2a8c9835..29cf511f 100644 --- a/src/sip/RingTone.cpp +++ b/src/sip/RingTone.cpp @@ -1,6 +1,6 @@ #include "RingTone.h" #include "AudioManager.h" -#include "ReadOnlyConfdSettings.h" +#include "AudioPort.h" RingTone::RingTone(quint16 frequency1, quint16 frequency2, QList> intervals, qint8 loopIndex, QObject *parent) @@ -40,10 +40,17 @@ void RingTone::start() m_isPlaying = true; m_currentIndex = 0; + AudioManager::instance().acquireDevice(); + if (m_stopTimer.isActive()) { m_stopTimer.stop(); } + // Bridge gap between previous audio source (mostly the call) + if (auto *port = dynamic_cast(&m_mediaSink)) { + port->writeSilenceMS(120); + } + m_toneGen.startTransmit(m_mediaSink); playNextTone(); } @@ -55,20 +62,23 @@ void RingTone::stop() if (!m_isPlaying) { return; } + if (m_loopTimer.isActive()) { m_loopTimer.stop(); } + m_currentIndex = 0; m_isPlaying = false; m_toneGen.stop(); m_toneGen.stopTransmit(m_mediaSink); + AudioManager::instance().releaseDevice(); + Q_EMIT ready(); } void RingTone::playNextTone() { - // Create and play tone const auto &tuple = m_intervals.at(m_currentIndex); @@ -92,16 +102,22 @@ void RingTone::playNextTone() if (m_repeatTimes > 0) { --m_repeatTimes; } else if (m_repeatTimes == 0) { - stop(); + scheduleStop(tuple.first + tuple.second); return; } } else { - // No loop - stop the tone - m_stopTimer.setInterval(tuple.first + tuple.second); - m_stopTimer.start(); + scheduleStop(tuple.first + tuple.second); return; } } m_loopTimer.start(tuple.first + tuple.second); } + +void RingTone::scheduleStop(unsigned delay) +{ + static constexpr unsigned PIPELINE_GRACE_MS = 400; + + m_stopTimer.setInterval(delay + PIPELINE_GRACE_MS); + m_stopTimer.start(); +} diff --git a/src/sip/RingTone.h b/src/sip/RingTone.h index da72ea14..a2bc4097 100644 --- a/src/sip/RingTone.h +++ b/src/sip/RingTone.h @@ -60,6 +60,8 @@ private Q_SLOTS: void playNextTone(); private: + void scheduleStop(unsigned delay); + bool m_isPlaying = false; pj::ToneGenerator m_toneGen; pj::AudioMedia &m_mediaSink; diff --git a/src/sip/SIPAccount.cpp b/src/sip/SIPAccount.cpp index c8bde695..a99eaaa3 100644 --- a/src/sip/SIPAccount.cpp +++ b/src/sip/SIPAccount.cpp @@ -857,7 +857,7 @@ void SIPAccount::removeCall(SIPCall *call) { if (call) { m_calls.removeAll(call); - delete call; + call->deleteLater(); } } diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index a4274e8e..8f5f9f1b 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -79,6 +79,8 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool } } + AudioManager::instance().acquireDevice(); + // Setup rtt timeout m_rttTimeoutTimer.setSingleShot(true); m_rttTimeoutTimer.setInterval(6s); @@ -96,6 +98,8 @@ SIPCall::SIPCall(SIPAccount *account, int callId, const QString &contactId, bool SIPCall::~SIPCall() { + AudioManager::instance().releaseDevice(); + if (m_isEmergencyCall) { Q_EMIT ViewHelper::instance().hideEmergency(); } @@ -286,13 +290,14 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) if (m_isEstablished) { ringToneFactory.endTone()->start(); - } - if (!m_incoming) { - if (statusCode == PJSIP_SC_BUSY_HERE) { - ringToneFactory.busyTone()->start(5); - } else if (static_cast(statusCode) >= 400 - && static_cast(statusCode) < 700) { - ringToneFactory.congestionTone()->start(5); + } else { + if (!m_incoming) { + if (statusCode == PJSIP_SC_BUSY_HERE) { + ringToneFactory.busyTone()->start(5); + } else if (static_cast(statusCode) >= 400 + && static_cast(statusCode) < 700) { + ringToneFactory.congestionTone()->start(5); + } } } } diff --git a/src/usb/HeadsetDeviceProxy.cpp b/src/usb/HeadsetDeviceProxy.cpp index a39cbf38..7cdd3449 100644 --- a/src/usb/HeadsetDeviceProxy.cpp +++ b/src/usb/HeadsetDeviceProxy.cpp @@ -36,6 +36,19 @@ HeadsetDeviceProxy::HeadsetDeviceProxy(QObject *parent) : IHeadsetDevice(parent) &HeadsetDeviceProxy::updateRemoteContactInfo); connect(&GlobalCallState::instance(), &GlobalCallState::isPhoneConferenceChanged, this, &HeadsetDeviceProxy::updateRemoteContactInfo); + + m_callEndTimer.setInterval(2s); + m_callEndTimer.setSingleShot(true); + connect(&m_callEndTimer, &QTimer::timeout, this, [this]() { + if (!m_device) { + return; + } + + if (GlobalCallState::instance().globalCallState() == ICallState::State::Idle) { + m_device->setCallStatus(tr("Call ended")); + m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::HomeScreen); + } + }); } HeadsetDeviceProxy::~HeadsetDeviceProxy() @@ -56,6 +69,11 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) refreshAll ? ICallState::States::fromInt((1 << 9) - 1) : (m_oldCallState ^ state); m_oldCallState = state; + + if (state && m_callEndTimer.isActive()) { + m_callEndTimer.stop(); + } + if (changeMask & State::RingingIncoming) { setRing(state & State::RingingIncoming); if (state & State::RingingIncoming) { @@ -107,13 +125,16 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) if (changeMask && !state) { setIdle(); m_inRemoteCallScreen = false; - m_device->setCallStatus(tr("Call ended")); - m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::HomeScreen); + m_callEndTimer.start(); } if (changeMask & State::AudioActive && !(state & State::Migrating)) { GlobalMuteState::instance().reset(); - setBusyLine(state & State::AudioActive); + if (state & State::AudioActive) { + setBusyLine(true); + } else { + setBusyLine(false); + } } if (changeMask & State::CallActive) { @@ -121,10 +142,6 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) m_inRemoteCallScreen = true; m_device->setCallStatus(tr("Call active")); m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::InCall); - } else { - m_inRemoteCallScreen = false; - m_device->setCallStatus(tr("Call ended")); - m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::EndCall); } } diff --git a/src/usb/HeadsetDeviceProxy.h b/src/usb/HeadsetDeviceProxy.h index 87bce601..74554df4 100644 --- a/src/usb/HeadsetDeviceProxy.h +++ b/src/usb/HeadsetDeviceProxy.h @@ -66,6 +66,8 @@ protected Q_SLOTS: HeadsetDevice *m_device = nullptr; ICallState::States m_oldCallState = ICallState::State::Idle; + QTimer m_callEndTimer; + bool m_inRemoteCallScreen = false; QString m_muteTag; }; From c52eea657166b865f6cfdb72e7ec259059c7b063 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Mon, 1 Jun 2026 12:33:00 +0200 Subject: [PATCH 2/4] chore: formatting --- src/media/AudioPort.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/media/AudioPort.cpp b/src/media/AudioPort.cpp index 8cfa2ed8..d019abce 100644 --- a/src/media/AudioPort.cpp +++ b/src/media/AudioPort.cpp @@ -340,7 +340,7 @@ void AudioPort::acquire() if (m_device.mode() == QAudioDevice::Mode::Input && !m_io.isNull()) { QObject::disconnect(m_warmUpDrain); - m_warmUpDrain = connect(m_io.data(), &QIODevice::readyRead, this, [this]() { + m_warmUpDrain = connect(m_io.data(), &QIODevice::readyRead, this, [this]() { if (m_isWarmingUp && !m_io.isNull()) { m_io->readAll(); } From dd2889f10aa7dfe0ff76fe2f88c55f62c7161b87 Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Mon, 1 Jun 2026 14:00:39 +0200 Subject: [PATCH 3/4] Tune call creation --- src/sip/SIPCall.cpp | 5 +++++ src/usb/HeadsetDeviceProxy.cpp | 33 ++++++++++++++++++++++++--------- src/usb/HeadsetDeviceProxy.h | 1 + 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 8f5f9f1b..7bd5f304 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -6,6 +6,7 @@ #include "SIPCallManager.h" #include "SIPAccount.h" #include "AudioManager.h" +#include "AudioPort.h" #include "ResponseLoader.h" #include "RingToneFactory.h" #include "RingTone.h" @@ -375,6 +376,10 @@ void SIPCall::onCallMediaState(pj::OnCallMediaStateParam &prm) tr("Failed to initialize microphone audio")); } + if (auto *port = dynamic_cast(&speaker_media)) { + port->writeSilenceMS(120); + } + try { aud_med.startTransmit(speaker_media); } catch (pj::Error &err) { diff --git a/src/usb/HeadsetDeviceProxy.cpp b/src/usb/HeadsetDeviceProxy.cpp index 7cdd3449..4aef3580 100644 --- a/src/usb/HeadsetDeviceProxy.cpp +++ b/src/usb/HeadsetDeviceProxy.cpp @@ -49,6 +49,22 @@ HeadsetDeviceProxy::HeadsetDeviceProxy(QObject *parent) : IHeadsetDevice(parent) m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::HomeScreen); } }); + + m_callStartTimer.setInterval(500ms); + m_callStartTimer.setSingleShot(true); + connect(&m_callStartTimer, &QTimer::timeout, this, [this]() { + if (!m_device) { + return; + } + + const auto state = GlobalCallState::instance().globalCallState(); + if ((state & ICallState::State::CallActive) && !(state & ICallState::State::OnHold)) { + m_inRemoteCallScreen = true; + m_device->setCallStatus(tr("Call active")); + m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::InCall); + updateRemoteContactInfo(); + } + }); } HeadsetDeviceProxy::~HeadsetDeviceProxy() @@ -91,8 +107,7 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::IncomingCall); } else if (state & State::CallActive) { m_inRemoteCallScreen = true; - m_device->setCallStatus(tr("Call active")); - m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::InCall); + m_callStartTimer.start(); } } @@ -103,8 +118,7 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::OutgoingCall); } else if (state & State::CallActive) { m_inRemoteCallScreen = true; - m_device->setCallStatus(tr("Call active")); - m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::InCall); + m_callStartTimer.start(); } } @@ -116,14 +130,14 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::HoldCall); } else { m_inRemoteCallScreen = true; - m_device->setCallStatus(tr("Call active")); - m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::InCall); + m_callStartTimer.start(); } } // IDLE if (changeMask && !state) { setIdle(); + m_callStartTimer.stop(); m_inRemoteCallScreen = false; m_callEndTimer.start(); } @@ -140,12 +154,13 @@ void HeadsetDeviceProxy::updateDeviceState(bool refreshAll) if (changeMask & State::CallActive) { if (state & State::CallActive) { m_inRemoteCallScreen = true; - m_device->setCallStatus(tr("Call active")); - m_device->selectScreen(ReportDescriptorEnums::TeamsScreenSelect::InCall); + m_callStartTimer.start(); } } - updateRemoteContactInfo(); + if (!m_callStartTimer.isActive()) { + updateRemoteContactInfo(); + } } void HeadsetDeviceProxy::updateRemoteContactInfo() diff --git a/src/usb/HeadsetDeviceProxy.h b/src/usb/HeadsetDeviceProxy.h index 74554df4..ce7a49c3 100644 --- a/src/usb/HeadsetDeviceProxy.h +++ b/src/usb/HeadsetDeviceProxy.h @@ -66,6 +66,7 @@ protected Q_SLOTS: HeadsetDevice *m_device = nullptr; ICallState::States m_oldCallState = ICallState::State::Idle; + QTimer m_callStartTimer; QTimer m_callEndTimer; bool m_inRemoteCallScreen = false; From befb3b436007846fa4618231f9800b8ad053b38d Mon Sep 17 00:00:00 2001 From: Cajus Pollmeier Date: Tue, 2 Jun 2026 08:37:47 +0200 Subject: [PATCH 4/4] Remove left over --- src/media/AudioPort.cpp | 2 +- src/sip/SIPCall.cpp | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/media/AudioPort.cpp b/src/media/AudioPort.cpp index d019abce..6c5aec29 100644 --- a/src/media/AudioPort.cpp +++ b/src/media/AudioPort.cpp @@ -203,7 +203,7 @@ void AudioPort::stopSinkIO() writeSilenceMS(SILENCE_BUFFER_MS); m_isDraining = true; - QTimer::singleShot(SILENCE_BUFFER_MS + 200, this, [this]() { + QTimer::singleShot(SILENCE_BUFFER_MS, this, [this]() { m_isDraining = false; if (m_sink) { diff --git a/src/sip/SIPCall.cpp b/src/sip/SIPCall.cpp index 7bd5f304..92aa08ff 100644 --- a/src/sip/SIPCall.cpp +++ b/src/sip/SIPCall.cpp @@ -291,14 +291,12 @@ void SIPCall::onCallState(pj::OnCallStateParam &prm) if (m_isEstablished) { ringToneFactory.endTone()->start(); - } else { - if (!m_incoming) { - if (statusCode == PJSIP_SC_BUSY_HERE) { - ringToneFactory.busyTone()->start(5); - } else if (static_cast(statusCode) >= 400 - && static_cast(statusCode) < 700) { - ringToneFactory.congestionTone()->start(5); - } + } else if (!m_incoming) { + if (statusCode == PJSIP_SC_BUSY_HERE) { + ringToneFactory.busyTone()->start(5); + } else if (static_cast(statusCode) >= 400 + && static_cast(statusCode) < 700) { + ringToneFactory.congestionTone()->start(5); } } }