|
|
|
|
@ -1,5 +1,7 @@
|
|
|
|
|
#include "audiodevicesettingspage.h" |
|
|
|
|
#include "sound_manager.h" |
|
|
|
|
#include "audiorecorder.h" |
|
|
|
|
#include "voicemessageencoder.h" |
|
|
|
|
#include "../db/db_manager.h" |
|
|
|
|
#include "../transport/gui_bridge.h" |
|
|
|
|
#include "../../lib/mem.h" |
|
|
|
|
@ -7,9 +9,12 @@ extern "C" {
|
|
|
|
|
#include "../transport/chat_core.h" |
|
|
|
|
} |
|
|
|
|
#include <QVBoxLayout> |
|
|
|
|
#include <QHBoxLayout> |
|
|
|
|
#include <QLabel> |
|
|
|
|
#include <QFormLayout> |
|
|
|
|
#include <QFrame> |
|
|
|
|
#include <QDebug> |
|
|
|
|
#include <cmath> |
|
|
|
|
|
|
|
|
|
AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, QWidget* parent) |
|
|
|
|
: QWidget(parent), m_db(db) |
|
|
|
|
@ -17,13 +22,8 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, QWidget* parent)
|
|
|
|
|
auto* layout = new QVBoxLayout(this); |
|
|
|
|
layout->setContentsMargins(0, 12, 0, 0); |
|
|
|
|
|
|
|
|
|
auto* infoLabel = new QLabel("Select audio devices for playback and recording.\nChanges take effect after restart.", this); |
|
|
|
|
infoLabel->setWordWrap(true); |
|
|
|
|
infoLabel->setStyleSheet("color: palette(text); font-size: 12px;"); |
|
|
|
|
layout->addWidget(infoLabel); |
|
|
|
|
|
|
|
|
|
auto* formLayout = new QFormLayout(); |
|
|
|
|
formLayout->setContentsMargins(0, 16, 0, 0); |
|
|
|
|
formLayout->setContentsMargins(0, 0, 0, 0); |
|
|
|
|
formLayout->setSpacing(10); |
|
|
|
|
|
|
|
|
|
auto* outLabel = new QLabel("Playback device:", this); |
|
|
|
|
@ -39,48 +39,142 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, QWidget* parent)
|
|
|
|
|
formLayout->addRow(inLabel, m_inputCombo); |
|
|
|
|
|
|
|
|
|
layout->addLayout(formLayout); |
|
|
|
|
|
|
|
|
|
/* --- Separator --- */ |
|
|
|
|
auto* sep1 = new QFrame(this); |
|
|
|
|
sep1->setFrameShape(QFrame::HLine); |
|
|
|
|
sep1->setStyleSheet("QFrame { color: palette(mid); }"); |
|
|
|
|
layout->addWidget(sep1); |
|
|
|
|
|
|
|
|
|
/* --- Microphone test --- */ |
|
|
|
|
auto* micLabel = new QLabel("Microphone test", this); |
|
|
|
|
micLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); |
|
|
|
|
layout->addWidget(micLabel); |
|
|
|
|
|
|
|
|
|
auto* micRow = new QHBoxLayout(); |
|
|
|
|
micRow->setSpacing(8); |
|
|
|
|
m_micLevelBar = new QProgressBar(this); |
|
|
|
|
m_micLevelBar->setRange(0, 100); |
|
|
|
|
m_micLevelBar->setValue(0); |
|
|
|
|
m_micLevelBar->setTextVisible(false); |
|
|
|
|
m_micLevelBar->setFixedHeight(16); |
|
|
|
|
m_micLevelBar->setStyleSheet( |
|
|
|
|
"QProgressBar { border: 1px solid palette(mid); border-radius: 4px; background: #333; }" |
|
|
|
|
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0," |
|
|
|
|
" stop:0 #27ae60, stop:0.6 #f1c40f, stop:1 #e74c3c); border-radius: 3px; }"); |
|
|
|
|
micRow->addWidget(m_micLevelBar, 1); |
|
|
|
|
|
|
|
|
|
m_testMicBtn = new QPushButton("Test Microphone", this); |
|
|
|
|
m_testMicBtn->setCheckable(true); |
|
|
|
|
m_testMicBtn->setStyleSheet( |
|
|
|
|
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;" |
|
|
|
|
" padding: 4px 12px; font-size: 12px; background: palette(button); }" |
|
|
|
|
"QPushButton:checked { background: #c0392b; color: white; }"); |
|
|
|
|
connect(m_testMicBtn, &QPushButton::toggled, this, &AudioDeviceSettingsPage::onTestMicToggled); |
|
|
|
|
micRow->addWidget(m_testMicBtn); |
|
|
|
|
layout->addLayout(micRow); |
|
|
|
|
|
|
|
|
|
/* --- Separator --- */ |
|
|
|
|
auto* sep2 = new QFrame(this); |
|
|
|
|
sep2->setFrameShape(QFrame::HLine); |
|
|
|
|
sep2->setStyleSheet("QFrame { color: palette(mid); }"); |
|
|
|
|
layout->addWidget(sep2); |
|
|
|
|
|
|
|
|
|
/* --- Speaker test --- */ |
|
|
|
|
auto* spkLabel = new QLabel("Speaker test", this); |
|
|
|
|
spkLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); |
|
|
|
|
layout->addWidget(spkLabel); |
|
|
|
|
|
|
|
|
|
m_testSpeakerBtn = new QPushButton("Test Speaker (440Hz)", this); |
|
|
|
|
m_testSpeakerBtn->setStyleSheet( |
|
|
|
|
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;" |
|
|
|
|
" padding: 6px 16px; font-size: 13px; background: palette(button); }" |
|
|
|
|
"QPushButton:hover { background: palette(light); }"); |
|
|
|
|
connect(m_testSpeakerBtn, &QPushButton::clicked, this, &AudioDeviceSettingsPage::onTestSpeaker); |
|
|
|
|
layout->addWidget(m_testSpeakerBtn); |
|
|
|
|
|
|
|
|
|
/* --- Separator --- */ |
|
|
|
|
auto* sep3 = new QFrame(this); |
|
|
|
|
sep3->setFrameShape(QFrame::HLine); |
|
|
|
|
sep3->setStyleSheet("QFrame { color: palette(mid); }"); |
|
|
|
|
layout->addWidget(sep3); |
|
|
|
|
|
|
|
|
|
/* --- Codec quality --- */ |
|
|
|
|
auto* codecLabel = new QLabel("Voice codec quality", this); |
|
|
|
|
codecLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); |
|
|
|
|
layout->addWidget(codecLabel); |
|
|
|
|
|
|
|
|
|
m_codecPresetCombo = new QComboBox(this); |
|
|
|
|
m_codecPresetCombo->addItem("Low (16 kbps, fast)", 0); |
|
|
|
|
m_codecPresetCombo->addItem("Standard (32 kbps)", 1); |
|
|
|
|
m_codecPresetCombo->addItem("High (64 kbps, best)", 2); |
|
|
|
|
m_codecPresetCombo->setStyleSheet(m_outputCombo->styleSheet()); |
|
|
|
|
layout->addWidget(m_codecPresetCombo); |
|
|
|
|
|
|
|
|
|
layout->addStretch(); |
|
|
|
|
|
|
|
|
|
m_levelTimer = new QTimer(this); |
|
|
|
|
m_levelTimer->setInterval(100); |
|
|
|
|
connect(m_levelTimer, &QTimer::timeout, this, &AudioDeviceSettingsPage::onLevelTimer); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
AudioDeviceSettingsPage::~AudioDeviceSettingsPage() { |
|
|
|
|
stopMicTest(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioDeviceSettingsPage::loadFromDb() { |
|
|
|
|
auto playbackDevs = SoundManager::enumeratePlaybackDevices(); |
|
|
|
|
auto captureDevs = SoundManager::enumerateCaptureDevices(); |
|
|
|
|
|
|
|
|
|
/* Output devices */ |
|
|
|
|
m_outputCombo->clear(); |
|
|
|
|
m_outputCombo->addItem("Default (auto)", -1); |
|
|
|
|
for (int i = 0; i < playbackDevs.size(); i++) { |
|
|
|
|
QString label = playbackDevs[i].name; |
|
|
|
|
if (playbackDevs[i].isDefault) label += " (default)"; |
|
|
|
|
m_outputCombo->addItem(label, i); |
|
|
|
|
if (playbackDevs[i].isDefault && m_savedOutputIdx < 0) m_savedOutputIdx = i; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Input devices */ |
|
|
|
|
m_inputCombo->clear(); |
|
|
|
|
m_inputCombo->addItem("Default (auto)", -1); |
|
|
|
|
for (int i = 0; i < captureDevs.size(); i++) { |
|
|
|
|
QString label = captureDevs[i].name; |
|
|
|
|
if (captureDevs[i].isDefault) label += " (default)"; |
|
|
|
|
m_inputCombo->addItem(label, i); |
|
|
|
|
if (captureDevs[i].isDefault && m_savedInputIdx < 0) m_savedInputIdx = i; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (m_db) { |
|
|
|
|
m_savedOutputIdx = m_db->getUiStateInt("audio_output_device", m_savedOutputIdx); |
|
|
|
|
m_savedInputIdx = m_db->getUiStateInt("audio_input_device", m_savedInputIdx); |
|
|
|
|
m_savedOutputIdx = m_db->getUiStateInt("audio_output_device", -1); |
|
|
|
|
m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1); |
|
|
|
|
m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (m_savedOutputIdx >= 0 && m_savedOutputIdx < m_outputCombo->count()) |
|
|
|
|
m_outputCombo->setCurrentIndex(m_savedOutputIdx); |
|
|
|
|
if (m_savedInputIdx >= 0 && m_savedInputIdx < m_inputCombo->count()) |
|
|
|
|
m_inputCombo->setCurrentIndex(m_savedInputIdx); |
|
|
|
|
/* Select saved indices */ |
|
|
|
|
for (int i = 0; i < m_outputCombo->count(); i++) { |
|
|
|
|
if (m_outputCombo->itemData(i).toInt() == m_savedOutputIdx) |
|
|
|
|
{ m_outputCombo->setCurrentIndex(i); break; } |
|
|
|
|
} |
|
|
|
|
for (int i = 0; i < m_inputCombo->count(); i++) { |
|
|
|
|
if (m_inputCombo->itemData(i).toInt() == m_savedInputIdx) |
|
|
|
|
{ m_inputCombo->setCurrentIndex(i); break; } |
|
|
|
|
} |
|
|
|
|
for (int i = 0; i < m_codecPresetCombo->count(); i++) { |
|
|
|
|
if (m_codecPresetCombo->itemData(i).toInt() == m_savedCodecPreset) |
|
|
|
|
{ m_codecPresetCombo->setCurrentIndex(i); break; } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioDeviceSettingsPage::applyAndSave() { |
|
|
|
|
int outIdx = m_outputCombo->currentData().toInt(); |
|
|
|
|
int inIdx = m_inputCombo->currentData().toInt(); |
|
|
|
|
int codecPreset = m_codecPresetCombo->currentData().toInt(); |
|
|
|
|
|
|
|
|
|
auto saveKey = [](const char* key, int val) { |
|
|
|
|
QByteArray v = QByteArray::number(val); |
|
|
|
|
size_t klen = strlen(key), vlen = (size_t)v.size(); |
|
|
|
|
size_t total = klen + 1 + vlen + 1; |
|
|
|
|
if (total > 256) return; |
|
|
|
|
void* arg = u_malloc(total); |
|
|
|
|
memcpy(arg, key, klen + 1); |
|
|
|
|
memcpy((char*)arg + klen + 1, v.constData(), vlen + 1); |
|
|
|
|
@ -88,8 +182,65 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
|
|
|
|
}; |
|
|
|
|
saveKey("audio_output_device", outIdx); |
|
|
|
|
saveKey("audio_input_device", inIdx); |
|
|
|
|
saveKey("opus_codec_preset", codecPreset); |
|
|
|
|
|
|
|
|
|
VoiceEncoder::setPreset(codecPreset); |
|
|
|
|
|
|
|
|
|
m_savedOutputIdx = outIdx; |
|
|
|
|
m_savedInputIdx = inIdx; |
|
|
|
|
qDebug("AudioDeviceSettings: saved output=%d input=%d", outIdx, inIdx); |
|
|
|
|
m_savedCodecPreset = codecPreset; |
|
|
|
|
qDebug("AudioDeviceSettings: saved output=%d input=%d codec=%d", outIdx, inIdx, codecPreset); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioDeviceSettingsPage::onTestMicToggled() { |
|
|
|
|
if (m_testMicBtn->isChecked()) { |
|
|
|
|
m_testRecorder = new AudioRecorder(this); |
|
|
|
|
m_testRecorder->init(); |
|
|
|
|
int inIdx = m_inputCombo->currentData().toInt(); |
|
|
|
|
m_testRecorder->setCaptureDeviceIndex(inIdx); |
|
|
|
|
m_testRecorder->startRecording(); |
|
|
|
|
m_micTestActive = true; |
|
|
|
|
m_levelTimer->start(); |
|
|
|
|
m_testMicBtn->setText("Stop Test"); |
|
|
|
|
m_micLevelBar->setValue(0); |
|
|
|
|
} else { |
|
|
|
|
stopMicTest(); |
|
|
|
|
m_testMicBtn->setText("Test Microphone"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioDeviceSettingsPage::stopMicTest() { |
|
|
|
|
m_levelTimer->stop(); |
|
|
|
|
if (m_testRecorder) { |
|
|
|
|
m_testRecorder->stopRecording(); |
|
|
|
|
m_testRecorder->shutdown(); |
|
|
|
|
m_testRecorder->deleteLater(); |
|
|
|
|
m_testRecorder = nullptr; |
|
|
|
|
} |
|
|
|
|
m_micTestActive = false; |
|
|
|
|
m_micLevelBar->setValue(0); |
|
|
|
|
m_testMicBtn->setChecked(false); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioDeviceSettingsPage::onLevelTimer() { |
|
|
|
|
if (!m_micTestActive || !m_testRecorder || !m_testRecorder->isRecording()) { |
|
|
|
|
stopMicTest(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
float peak = m_testRecorder->peakLevel(); |
|
|
|
|
/* Linear to dB: 20*log10(peak), map -60..0 dB to 0..100 */ |
|
|
|
|
float dbVal = peak > 0.0001f ? 20.0f * std::log10(peak) : -70.0f; |
|
|
|
|
int pct = (int)((dbVal + 70.0f) / 70.0f * 100.0f); |
|
|
|
|
if (pct < 0) pct = 0; |
|
|
|
|
if (pct > 100) pct = 100; |
|
|
|
|
m_micLevelBar->setValue(pct); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void AudioDeviceSettingsPage::onTestSpeaker() { |
|
|
|
|
int sampleRate = 48000; |
|
|
|
|
int samples = sampleRate / 4; /* 250ms */ |
|
|
|
|
std::vector<int16_t> pcm(samples); |
|
|
|
|
for (int i = 0; i < samples; i++) |
|
|
|
|
pcm[i] = (int16_t)(12000.0 * sin(2.0 * M_PI * 440.0 * i / sampleRate)); |
|
|
|
|
SoundManager::instance()->playRawPcm(pcm.data(), samples, sampleRate, 1); |
|
|
|
|
} |
|
|
|
|
|