diff --git a/.github/workflows/sdrangel.yml b/.github/workflows/sdrangel.yml index 79bd00b0a..823ee176e 100644 --- a/.github/workflows/sdrangel.yml +++ b/.github/workflows/sdrangel.yml @@ -7,6 +7,8 @@ on: branches: - master - mac_ci + - fix-* + - feature-* tags: - 'v*' pull_request: diff --git a/doc/img/DenoiserFeature_plugin.png b/doc/img/DenoiserFeature_plugin.png new file mode 100644 index 000000000..3ef869fa2 Binary files /dev/null and b/doc/img/DenoiserFeature_plugin.png differ diff --git a/doc/img/DenoiserFeature_plugin.xcf b/doc/img/DenoiserFeature_plugin.xcf new file mode 100644 index 000000000..d4307b0db Binary files /dev/null and b/doc/img/DenoiserFeature_plugin.xcf differ diff --git a/plugins/feature/denoiser/denoiser.cpp b/plugins/feature/denoiser/denoiser.cpp index 03a6d8a28..4df8d7cc3 100644 --- a/plugins/feature/denoiser/denoiser.cpp +++ b/plugins/feature/denoiser/denoiser.cpp @@ -146,6 +146,11 @@ void Denoiser::start() } } + if (m_levelMeter) { + connect(m_worker, SIGNAL(levelChanged(qreal, qreal, int)), m_levelMeter, SLOT(levelChanged(qreal, qreal, int))); + } + + m_running = true; } @@ -445,6 +450,15 @@ void Denoiser::webapiFormatFeatureSettings( SWGSDRangel::SWGFeatureSettings& response, const DenoiserSettings& settings) { + response.getDenoiserSettings()->setDenoiserType(static_cast(settings.m_denoiserType)); + response.getDenoiserSettings()->setEnableDenoiser(settings.m_enableDenoiser ? 1 : 0); + response.getDenoiserSettings()->setAudioMute(settings.m_audioMute ? 1 : 0); + response.getDenoiserSettings()->setVolumeTenths(settings.m_volumeTenths); + if (response.getDenoiserSettings()->getAudioDeviceName()) { + *response.getDenoiserSettings()->getAudioDeviceName() = settings.m_audioDeviceName; + } else { + response.getDenoiserSettings()->setAudioDeviceName(new QString(settings.m_audioDeviceName)); + } if (response.getDenoiserSettings()->getTitle()) { *response.getDenoiserSettings()->getTitle() = settings.m_title; } else { @@ -492,6 +506,21 @@ void Denoiser::webapiUpdateFeatureSettings( const QStringList& featureSettingsKeys, SWGSDRangel::SWGFeatureSettings& response) { + if (featureSettingsKeys.contains("DenoiserType")) { + settings.m_denoiserType = static_cast(response.getDenoiserSettings()->getDenoiserType()); + } + if (featureSettingsKeys.contains("enableDenoiser")) { + settings.m_enableDenoiser = response.getDenoiserSettings()->getEnableDenoiser() != 0; + } + if (featureSettingsKeys.contains("audioMute")) { + settings.m_audioMute = response.getDenoiserSettings()->getAudioMute() != 0; + } + if (featureSettingsKeys.contains("volumeTenths")) { + settings.m_volumeTenths = response.getDenoiserSettings()->getVolumeTenths(); + } + if (featureSettingsKeys.contains("audioDeviceName")) { + settings.m_audioDeviceName = *response.getDenoiserSettings()->getAudioDeviceName(); + } if (featureSettingsKeys.contains("title")) { settings.m_title = *response.getDenoiserSettings()->getTitle(); } @@ -535,6 +564,36 @@ void Denoiser::webapiReverseSendSettings(const QList& featureSettingsKe // transfer data that has been modified. When force is on transfer all data except reverse API data + if (featureSettingsKeys.contains("useReverseAPI") || force) { + swgDenoiserSettings->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + } + if (featureSettingsKeys.contains("reverseAPIAddress") || force) { + swgDenoiserSettings->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + if (featureSettingsKeys.contains("reverseAPIPort") || force) { + swgDenoiserSettings->setReverseApiPort(settings.m_reverseAPIPort); + } + if (featureSettingsKeys.contains("reverseAPIFeatureSetIndex") || force) { + swgDenoiserSettings->setReverseApiFeatureSetIndex(settings.m_reverseAPIFeatureSetIndex); + } + if (featureSettingsKeys.contains("reverseAPIFeatureIndex") || force) { + swgDenoiserSettings->setReverseApiFeatureIndex(settings.m_reverseAPIFeatureIndex); + } + if (featureSettingsKeys.contains("DenoiserType") || force) { + swgDenoiserSettings->setDenoiserType(static_cast(settings.m_denoiserType)); + } + if (featureSettingsKeys.contains("enableDenoiser") || force) { + swgDenoiserSettings->setEnableDenoiser(settings.m_enableDenoiser ? 1 : 0); + } + if (featureSettingsKeys.contains("audioMute") || force) { + swgDenoiserSettings->setAudioMute(settings.m_audioMute ? 1 : 0); + } + if (featureSettingsKeys.contains("volumeTenths") || force) { + swgDenoiserSettings->setVolumeTenths(settings.m_volumeTenths); + } + if (featureSettingsKeys.contains("audioDeviceName") || force) { + swgDenoiserSettings->setAudioDeviceName(new QString(settings.m_audioDeviceName)); + } if (featureSettingsKeys.contains("title") || force) { swgDenoiserSettings->setTitle(new QString(settings.m_title)); } diff --git a/plugins/feature/denoiser/denoiser.h b/plugins/feature/denoiser/denoiser.h index f95146995..63153233d 100644 --- a/plugins/feature/denoiser/denoiser.h +++ b/plugins/feature/denoiser/denoiser.h @@ -190,6 +190,7 @@ public: SWGSDRangel::SWGFeatureSettings& response); void getAvailableChannelsReport(); + void setLevelMeter(QObject *levelMeter) { m_levelMeter = levelMeter; } static const char* const m_featureIdURI; static const char* const m_featureId; @@ -205,6 +206,7 @@ private: ChannelAPI *m_selectedChannel; ObjectPipe *m_dataPipe; int m_sampleRate; + QObject *m_levelMeter = nullptr; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; diff --git a/plugins/feature/denoiser/denoisergui.cpp b/plugins/feature/denoiser/denoisergui.cpp index 58e398a3b..0041d2745 100644 --- a/plugins/feature/denoiser/denoisergui.cpp +++ b/plugins/feature/denoiser/denoisergui.cpp @@ -178,6 +178,7 @@ DenoiserGUI::DenoiserGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Featu DialPopup::addPopupsToChildDials(this); m_resizer.enableChildMouseTracking(); m_denoiser->getAvailableChannelsReport(); + m_denoiser->setLevelMeter(ui->volumeMeter); } DenoiserGUI::~DenoiserGUI() @@ -205,6 +206,12 @@ void DenoiserGUI::displaySettings() ui->record->setChecked(m_settings.m_recordToFile); ui->fileNameText->setText(m_settings.m_fileRecordName); ui->showFileDialog->setEnabled(!m_settings.m_recordToFile); + ui->denoiserType->setCurrentIndex(static_cast(m_settings.m_denoiserType)); + ui->enable->setChecked(m_settings.m_enableDenoiser); + ui->audioMute->setChecked(m_settings.m_audioMute); + ui->volume->setValue(m_settings.m_volumeTenths); + ui->volumeText->setText(QString::number(m_settings.m_volumeTenths / 10.0, 'f', 1)); + displayNRenabled(); getRollupContents()->restoreState(m_rollupState); blockApplySettings(false); } @@ -291,6 +298,10 @@ void DenoiserGUI::on_startStop_toggled(bool checked) { Denoiser::MsgStartStop *message = Denoiser::MsgStartStop::create(checked); m_denoiser->getInputMessageQueue()->push(message); + + if (checked && (ui->channels->count() > 0)) { + on_channels_currentIndexChanged(ui->channels->currentIndex()); + } } } @@ -347,6 +358,36 @@ void DenoiserGUI::on_showFileDialog_clicked(bool checked) } } +void DenoiserGUI::on_denoiserType_currentIndexChanged(int index) +{ + m_settings.m_denoiserType = static_cast(index); + m_settingsKeys.append("denoiserType"); + applySettings(); +} + +void DenoiserGUI::on_enable_toggled(bool checked) +{ + m_settings.m_enableDenoiser = checked; + displayNRenabled(); + m_settingsKeys.append("enableDenoiser"); + applySettings(); +} + +void DenoiserGUI::on_audioMute_toggled(bool checked) +{ + m_settings.m_audioMute = checked; + m_settingsKeys.append("audioMute"); + applySettings(); +} + +void DenoiserGUI::on_volume_valueChanged(int value) +{ + m_settings.m_volumeTenths = value; + ui->volumeText->setText(QString::number(value / 10.0, 'f', 1)); + m_settingsKeys.append("volumeTenths"); + applySettings(); +} + void DenoiserGUI::audioSelect(const QPoint& p) { qDebug("DenoiserGUI::audioSelect"); @@ -400,6 +441,15 @@ void DenoiserGUI::updateStatus() } } +void DenoiserGUI::displayNRenabled() +{ + if (m_settings.m_enableDenoiser) { + ui->enable->setStyleSheet("QToolButton { background-color : green; }"); + } else { + ui->enable->setStyleSheet("QToolButton { background-color : blue; }"); + } +} + void DenoiserGUI::applySettings(bool force) { if (m_doApplySettings) @@ -418,4 +468,8 @@ void DenoiserGUI::makeUIConnections() QObject::connect(ui->channelApply, &QPushButton::clicked, this, &DenoiserGUI::on_channelApply_clicked); QObject::connect(ui->record, &ButtonSwitch::toggled, this, &DenoiserGUI::on_record_toggled); QObject::connect(ui->showFileDialog, &QPushButton::clicked, this, &DenoiserGUI::on_showFileDialog_clicked); + QObject::connect(ui->denoiserType, qOverload(&QComboBox::currentIndexChanged), this, &DenoiserGUI::on_denoiserType_currentIndexChanged); + QObject::connect(ui->enable, &ButtonSwitch::toggled, this, &DenoiserGUI::on_enable_toggled); + QObject::connect(ui->audioMute, &ButtonSwitch::toggled, this, &DenoiserGUI::on_audioMute_toggled); + QObject::connect(ui->volume, &QDial::valueChanged, this, &DenoiserGUI::on_volume_valueChanged); } diff --git a/plugins/feature/denoiser/denoisergui.h b/plugins/feature/denoiser/denoisergui.h index 980a55464..afa3c8d32 100644 --- a/plugins/feature/denoiser/denoisergui.h +++ b/plugins/feature/denoiser/denoisergui.h @@ -78,6 +78,7 @@ private: void applySettings(bool force = false); void displaySettings(); void displaySampleRate(int sampleRate); + void displayNRenabled(); void updateChannelList(); bool handleMessage(const Message& message); void makeUIConnections(); @@ -91,6 +92,10 @@ private slots: void on_channelApply_clicked(); void on_record_toggled(bool checked); void on_showFileDialog_clicked(bool checked); + void on_denoiserType_currentIndexChanged(int index); + void on_enable_toggled(bool checked); + void on_audioMute_toggled(bool checked); + void on_volume_valueChanged(int value); void audioSelect(const QPoint& p); void updateStatus(); void tick(); diff --git a/plugins/feature/denoiser/denoisergui.ui b/plugins/feature/denoiser/denoisergui.ui index e708c6d14..b9e716f9b 100644 --- a/plugins/feature/denoiser/denoisergui.ui +++ b/plugins/feature/denoiser/denoisergui.ui @@ -120,14 +120,11 @@ - (Re) apply channel selection - - - + (Re) associate with channel - :/checkmark.png:/checkmark.png + :/link.png:/link.png @@ -190,7 +187,7 @@ 0 - + Noise reduction scheme @@ -206,6 +203,27 @@ + + + + + 0 + 22 + + + + Denoiser on/off + + + + + + + :/play.png + :/stop.png:/play.png + + + @@ -311,7 +329,7 @@ - Level (% full range) top trace: average, bottom trace: instantaneous peak, tip: peak hold + Input level (% full range) top trace: average, bottom trace: instantaneous peak, tip: peak hold diff --git a/plugins/feature/denoiser/denoisersettings.cpp b/plugins/feature/denoiser/denoisersettings.cpp index 701cfdd8b..e695554f6 100644 --- a/plugins/feature/denoiser/denoisersettings.cpp +++ b/plugins/feature/denoiser/denoisersettings.cpp @@ -43,7 +43,9 @@ void DenoiserSettings::resetToDefaults() { m_denoiserType = DenoiserType::DenoiserType_RNnoise; m_title = "Denoiser"; + m_enableDenoiser = true; m_audioMute = false; + m_volumeTenths = 10; m_audioDeviceName = AudioDeviceManager::m_defaultDeviceName; m_rgbColor = 0xffd700; // gold m_useReverseAPI = false; @@ -62,8 +64,6 @@ QByteArray DenoiserSettings::serialize() const SimpleSerializer s(1); s.writeS32(1, static_cast(m_denoiserType)); - s.writeBool(14, m_audioMute); - s.writeString(15, m_audioDeviceName); s.writeString(2, m_title); s.writeU32(3, m_rgbColor); s.writeBool(4, m_useReverseAPI); @@ -80,6 +80,11 @@ QByteArray DenoiserSettings::serialize() const s.writeBlob(13, m_rollupState->serialize()); } + s.writeBool(14, m_audioMute); + s.writeString(15, m_audioDeviceName); + s.writeBool(16, m_enableDenoiser); + s.writeS32(17, m_volumeTenths); + return s.final(); } @@ -101,8 +106,6 @@ bool DenoiserSettings::deserialize(const QByteArray& data) d.readS32(1, &itmp, 1); m_denoiserType = static_cast(itmp); - d.readBool(14, &m_audioMute, false); - d.readString(15, &m_audioDeviceName, AudioDeviceManager::m_defaultDeviceName); d.readString(2, &m_title, "Denoiser"); d.readU32(3, &m_rgbColor, 0xffd700); // gold d.readBool(4, &m_useReverseAPI, false); @@ -130,6 +133,11 @@ bool DenoiserSettings::deserialize(const QByteArray& data) m_rollupState->deserialize(bytetmp); } + d.readBool(14, &m_audioMute, false); + d.readString(15, &m_audioDeviceName, AudioDeviceManager::m_defaultDeviceName); + d.readBool(16, &m_enableDenoiser, true); + d.readS32(17, &m_volumeTenths, 10); + return true; } else @@ -144,9 +152,15 @@ void DenoiserSettings::applySettings(const QStringList& settingsKeys, const Deno if (settingsKeys.contains("denoiserType")) { m_denoiserType = settings.m_denoiserType; } + if (settingsKeys.contains("enableDenoiser")) { + m_enableDenoiser = settings.m_enableDenoiser; + } if (settingsKeys.contains("audioMute")) { m_audioMute = settings.m_audioMute; } + if (settingsKeys.contains("volumeTenths")) { + m_volumeTenths = settings.m_volumeTenths; + } if (settingsKeys.contains("audioDeviceName")) { m_audioDeviceName = settings.m_audioDeviceName; } @@ -189,9 +203,15 @@ QString DenoiserSettings::getDebugString(const QStringList& settingsKeys, bool f if (settingsKeys.contains("denoiserType") || force) { debugString += QString("DenoiserType: %1 ").arg(static_cast(m_denoiserType)); } + if (settingsKeys.contains("enableDenoiser") || force) { + debugString += QString("Denoiser Enable: %1 ").arg(m_enableDenoiser ? "true" : "false"); + } if (settingsKeys.contains("audioMute") || force) { debugString += QString("Audio Mute: %1 ").arg(m_audioMute ? "true" : "false"); } + if (settingsKeys.contains("volumeTenths") || force) { + debugString += QString("Volume : %1 ").arg(m_volumeTenths/10.0); + } if (settingsKeys.contains("audioDeviceName") || force) { debugString += QString("Audio Device Name: %1 ").arg(m_audioDeviceName); } diff --git a/plugins/feature/denoiser/denoisersettings.h b/plugins/feature/denoiser/denoisersettings.h index 01e57b6a6..e75a26728 100644 --- a/plugins/feature/denoiser/denoisersettings.h +++ b/plugins/feature/denoiser/denoisersettings.h @@ -32,7 +32,9 @@ struct DenoiserSettings }; DenoiserType m_denoiserType; + bool m_enableDenoiser; bool m_audioMute; + int m_volumeTenths; QString m_audioDeviceName; QString m_title; quint32 m_rgbColor; diff --git a/plugins/feature/denoiser/denoiserworker.cpp b/plugins/feature/denoiser/denoiserworker.cpp index 96040cced..dc5e4a41d 100644 --- a/plugins/feature/denoiser/denoiserworker.cpp +++ b/plugins/feature/denoiser/denoiserworker.cpp @@ -18,9 +18,12 @@ #include "dsp/wavfilerecord.h" #include "audio/audiodevicemanager.h" #include "dsp/dspengine.h" +#include "rnnoise.h" #include "denoiserworker.h" +const int DenoiserWorker::m_levelNbSamples = 480; // 10 ms at 48 kHz + MESSAGE_CLASS_DEFINITION(DenoiserWorker::MsgConfigureDenoiserWorker, Message) MESSAGE_CLASS_DEFINITION(DenoiserWorker::MsgConnectFifo, Message) @@ -35,18 +38,21 @@ DenoiserWorker::DenoiserWorker(QObject *parent) : m_wavFileRecord(nullptr), m_recordSilenceNbSamples(0), m_recordSilenceCount(0), - m_nbBytes(0) + m_nbBytes(0), + m_rnnoiseFill(0) { m_audioBuffer.resize(4800); m_audioBufferFill = 0; m_audioFifo.setSize(4800 * 4); DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(getAudioFifo(), getInputMessageQueue()); + m_rnnoiseState = rnnoise_create(nullptr); } DenoiserWorker::~DenoiserWorker() { m_inputMessageQueue.clear(); DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(getAudioFifo()); + rnnoise_destroy(m_rnnoiseState); } void DenoiserWorker::reset() @@ -112,11 +118,11 @@ void DenoiserWorker::feedPart( if (m_settings.m_recordToFile && m_wavFileRecord) { - for (int is = 0; is < countSamples; is++) - { - const Sample& sample = m_sampleBuffer[is]; + for (const auto& sample : m_sampleBuffer) { writeSampleToFile(sample); } + + m_sampleBuffer.clear(); } } @@ -200,6 +206,7 @@ bool DenoiserWorker::handleMessage(const Message& cmd) void DenoiserWorker::applySettings(const DenoiserSettings& settings, const QStringList& settingsKeys, bool force) { QMutexLocker mutexLocker(&m_mutex); + qDebug() << "DenoiserWorker::applySettings" << settings.getDebugString(settingsKeys, force) << " force: " << force; if (settingsKeys.contains("fileRecordName") || force) { @@ -261,6 +268,10 @@ void DenoiserWorker::applySettings(const DenoiserSettings& settings, const QStri // TODO: handle sample rate change } + if (settingsKeys.contains("enableDenoiser") || settingsKeys.contains("denoiserType") || force) { + m_rnnoiseFill = 0; + } + if (force) { m_settings = settings; } else { @@ -309,6 +320,11 @@ void DenoiserWorker::handleData() m_dataFifo->readCommit((unsigned int) count); } + + qreal rmsLevel, peakLevel; + int numSamples; + getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); } void DenoiserWorker::processSample( @@ -317,32 +333,82 @@ void DenoiserWorker::processSample( int i ) { + // Periodic debug to verify runtime settings and branch selection + // static uint32_t s_dbgCount = 0; + // if ((s_dbgCount++ % 48000) == 0) { // approx. once per second at 48 kS/s + // qDebug() << "DenoiserWorker::processSample: dataType=" << (int)dataType + // << " enable=" << m_settings.m_enableDenoiser + // << " type=" << static_cast(m_settings.m_denoiserType); + // } + switch(dataType) { case DataFifo::DataTypeI16: { int16_t *s = (int16_t*) begin; double re = s[i] / (double) std::numeric_limits::max(); + calculateLevel(re * (m_settings.m_volumeTenths / 10.0)); m_magsq = re*re; m_channelPowerAvg(m_magsq); - m_sampleBuffer[i].setReal(re * SDR_RX_SCALEF); - m_sampleBuffer[i].setImag(0); - - m_audioBuffer[m_audioBufferFill].l = s[i]; - m_audioBuffer[m_audioBufferFill].r = s[i]; - ++m_audioBufferFill; - - if (m_audioBufferFill >= m_audioBuffer.size()) + if (!m_settings.m_enableDenoiser || m_settings.m_denoiserType == DenoiserSettings::DenoiserType::DenoiserType_None) { - std::size_t res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + // if ((s_dbgCount % 48000) == 1) { + // qDebug() << "DenoiserWorker::processSample[I16]: passthrough branch"; + // } + m_sampleBuffer.push_back(Sample(re * SDR_RX_SCALEF, 0)); + m_audioBuffer[m_audioBufferFill].l = s[i]; + m_audioBuffer[m_audioBufferFill].r = s[i]; + ++m_audioBufferFill; - if (res != m_audioBufferFill) + if (m_audioBufferFill >= m_audioBuffer.size()) { - qDebug("DenoiserWorker::processSample: %lu/%lu audio samples written", res, m_audioBufferFill); - m_audioFifo.clear(); + std::size_t res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + if (res != m_audioBufferFill) + { + qDebug("DenoiserWorker::processSample: %lu/%lu audio samples written", res, m_audioBufferFill); + m_audioFifo.clear(); + } + m_audioBufferFill = 0; } + } + else if (m_settings.m_denoiserType == DenoiserSettings::DenoiserType::DenoiserType_RNnoise) + { + // if ((s_dbgCount % 48000) == 1) { + // qDebug() << "DenoiserWorker::processSample[I16]: RNNoise branch"; + // } + // feed RNNoise input buffer + m_rnnoiseIn[m_rnnoiseFill] = static_cast(s[i])*(m_settings.m_volumeTenths / 10.0f); // already in [-32768..32767] range + m_rnnoiseFill++; - m_audioBufferFill = 0; + if (m_rnnoiseFill >= 480) + { + // process RNNoise frame + rnnoise_process_frame(m_rnnoiseState, m_rnnoiseOut, m_rnnoiseIn); + + // output RNNoise processed samples + for (int j = 0; j < 480; j++) + { + float outSample = m_rnnoiseOut[j]; + m_sampleBuffer.push_back(Sample(outSample, 0)); + int16_t audioSample = static_cast(outSample); + m_audioBuffer[m_audioBufferFill].l = audioSample; + m_audioBuffer[m_audioBufferFill].r = audioSample; + ++m_audioBufferFill; + + if (m_audioBufferFill >= m_audioBuffer.size()) + { + std::size_t res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + if (res != m_audioBufferFill) + { + qDebug("DenoiserWorker::processSample: %lu/%lu audio samples written", res, m_audioBufferFill); + m_audioFifo.clear(); + } + m_audioBufferFill = 0; + } + } + + m_rnnoiseFill = 0; + } } } break; @@ -350,29 +416,89 @@ void DenoiserWorker::processSample( int16_t *s = (int16_t*) begin; double re = s[2*i] / (double) std::numeric_limits::max(); double im = s[2*i+1] / (double) std::numeric_limits::max(); + calculateLevel((re + im) * (m_settings.m_volumeTenths / 20.0)); m_magsq = re*re + im*im; m_channelPowerAvg(m_magsq); - m_sampleBuffer[i].setReal(re * SDR_RX_SCALEF); - m_sampleBuffer[i].setImag(im * SDR_RX_SCALEF); - - m_audioBuffer[m_audioBufferFill].l = s[2*i]; - m_audioBuffer[m_audioBufferFill].r = s[2*i+1]; - ++m_audioBufferFill; - - if (m_audioBufferFill >= m_audioBuffer.size()) + if (!m_settings.m_enableDenoiser || m_settings.m_denoiserType == DenoiserSettings::DenoiserType::DenoiserType_None) { - std::size_t res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + // if ((s_dbgCount % 48000) == 1) { + // qDebug() << "DenoiserWorker::processSample[CI16]: passthrough branch"; + // } + m_sampleBuffer.push_back(Sample(re * SDR_RX_SCALEF, im * SDR_RX_SCALEF)); + m_audioBuffer[m_audioBufferFill].l = s[2*i]; + m_audioBuffer[m_audioBufferFill].r = s[2*i+1]; + ++m_audioBufferFill; - if (res != m_audioBufferFill) + if (m_audioBufferFill >= m_audioBuffer.size()) { - qDebug("DenoiserWorker::processSample: %lu/%lu audio samples written", res, m_audioBufferFill); - m_audioFifo.clear(); + std::size_t res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + if (res != m_audioBufferFill) + { + qDebug("DenoiserWorker::processSample: %lu/%lu audio samples written", res, m_audioBufferFill); + m_audioFifo.clear(); + } + m_audioBufferFill = 0; } + } + else if (m_settings.m_denoiserType == DenoiserSettings::DenoiserType::DenoiserType_RNnoise) + { + // if ((s_dbgCount % 48000) == 1) { + // qDebug() << "DenoiserWorker::processSample[CI16]: RNNoise branch"; + // } + // feed RNNoise input buffer + m_rnnoiseIn[m_rnnoiseFill] = static_cast(s[2*i] + s[2*i+1]) * (m_settings.m_volumeTenths / 20.0f); // average I/Q in [-32768..32767] range + m_rnnoiseFill++; - m_audioBufferFill = 0; + if (m_rnnoiseFill >= 480) + { + // process RNNoise frame + rnnoise_process_frame(m_rnnoiseState, m_rnnoiseOut, m_rnnoiseIn); + + // output RNNoise processed samples + for (int j = 0; j < 480; j++) + { + float outSample = m_rnnoiseOut[j]; + m_sampleBuffer.push_back(Sample(outSample, outSample)); + int16_t audioSample = static_cast(outSample); + m_audioBuffer[m_audioBufferFill].l = audioSample; + m_audioBuffer[m_audioBufferFill].r = audioSample; + ++m_audioBufferFill; + + if (m_audioBufferFill >= m_audioBuffer.size()) + { + std::size_t res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + if (res != m_audioBufferFill) + { + qDebug("DenoiserWorker::processSample: %lu/%lu audio samples written", res, m_audioBufferFill); + m_audioFifo.clear(); + } + m_audioBufferFill = 0; + } + } + + m_rnnoiseFill = 0; + } } } break; } } + +void DenoiserWorker::calculateLevel(const Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} diff --git a/plugins/feature/denoiser/denoiserworker.h b/plugins/feature/denoiser/denoiserworker.h index 4c912ff26..6a4b8e995 100644 --- a/plugins/feature/denoiser/denoiserworker.h +++ b/plugins/feature/denoiser/denoiserworker.h @@ -32,6 +32,7 @@ #include "denoisersettings.h" class WavFileRecord; +class DenoiseState; class DenoiserWorker : public QObject { Q_OBJECT @@ -87,8 +88,23 @@ public: void applySettings(const DenoiserSettings& settings, const QStringList& settingsKeys, bool force = false); double getMagSq() const { return m_magsq; } double getMagSqAvg() const { return (double) m_channelPowerAvg; } + void getLevels(qreal& rmsLevel, qreal& peakLevel, int& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevelOut; + numSamples = m_levelNbSamples; + } - private: +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: DataFifo *m_dataFifo; int m_sinkSampleRate; MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication @@ -105,8 +121,21 @@ public: AudioVector m_audioBuffer; AudioFifo m_audioFifo; std::size_t m_audioBufferFill; + DenoiseState *m_rnnoiseState; + float m_rnnoiseIn[480]; + float m_rnnoiseOut[480]; + int m_rnnoiseFill; + + quint32 m_levelCalcCount = 0; + qreal m_rmsLevel; + qreal m_peakLevelOut; + Real m_peakLevel = 0.0f; + Real m_levelSum = 0.0f; + QRecursiveMutex m_mutex; + static const int m_levelNbSamples; + AudioFifo *getAudioFifo() { return &m_audioFifo; } void feedPart( const QByteArray::const_iterator& begin, @@ -116,13 +145,12 @@ public: bool handleMessage(const Message& cmd); void writeSampleToFile(const Sample& sample); - void processSample( DataFifo::DataType dataType, const QByteArray::const_iterator& begin, int i ); - + void calculateLevel(const Real& sample); private slots: void handleInputMessages(); diff --git a/plugins/feature/denoiser/readme.md b/plugins/feature/denoiser/readme.md new file mode 100644 index 000000000..b2b6acafc --- /dev/null +++ b/plugins/feature/denoiser/readme.md @@ -0,0 +1,104 @@ +

Demoiser

+ +

Introduction

+ +This audio denoiser plugin can be used to reduce or remove noise from audio. For now it only implements the RNNoise noise reduction (more details next) + +It connects to the "demod" stream of Rx channels similarly to the Demod analyzer plugin. Hence it covers: + + - AM demodulator + - Broadcast FM demodulator + - NFM demodulator + - SSB demodulator + - WFM demodulator + - WDSP plugin (multimode) + +The following noise reduction schemes are covered. It can be selected via the (6) combo box: + +

RNNoise

+ +Noise reduction based on the RNnoise library originally from J.M. Valin. It uses a fork for easier integration in the build system (Cmake support with download of the model file): https://github.com/f4exb/rnnoise + +The noise reduction is based on a mix of DSP functions and a recursive neural network (RNN). Basically the RNN helps the DSP functions to adjust the gain in various spectral bands thus very efficiently cancelling the background noise in many situations. Although the model was not particularly trained on radio transmissions it makes a pretty good job at AM and SSB noise reduction however you will need a reasonable SNR to get something out of it else it will consider the audio is just noise. Do not expect it to dig signals out of the noise the goal is to reduce ear fatigue by removing background white noise and other noises e.g birdies. It is not good at FM transmissions. + +You will find all the details about RNnoise here: https://jmvalin.ca/demo/rnnoise/ + +Please note the following points: + + - Audio sample rate must be 48 kS/s (check 4) + - When taking the audio source from the WDSP plugin it should be used without noise reduction + - You should have enough input level but not exceed 100% on peaks (check 9 and 10). An average level between 10 and 20% should already provide good results + - The model has been trained on human voice therefore anything else like music is considered to be noise. It may however be successful at selecting the voice from songs. + - It should have enough original spectral components therefore any noise processing before the input will only deteriorate its performance. It should also have enough bandwidth it is recommended to have at least 100-3000 Hz. It is not an issue to extend beyond 3000 Hz because any high frequency hiss will be cancelled and it may benefit from the extra bandwidth on some transmissions. + - With SSB transmisions the pitch should be as close as possible to the natural pitch of the voice. In any case prefer a higher pitch to a lower one. Note that some voices are better processed than others which may also depend on voice processing before transmission. + +

Interface

+ +![Denoiser plugin GUI](../../../doc/img/DenoiserFeature_plugin.png) + +

1: Start/Stop plugin

+ +This button starts or stops the plugin + +

2: Channel selection

+ +Use this combo to select which channel to use for display. Channel is selected at start time and upon change. You may use button (3) to force association with the channel if necessary. + +

3: (Re)apply channel selection

+ +Applies or re-applies channel association (2) so that the channel gets effectively (re)connected to the denoiser. Normally it should not be necessary to use it. + +

4: Input sample rate

+ +This is the input audio stream sample rate and for RNNoise it should always be 48 kS/s + +

5: Input power

+ +Indication of the input audio stream power + +

6: Noise reduction scheme

+ +Selects the noise reduction scheme + + - **None**: No noise reduction (passthrough) + - **RNnoise**: RNNoise (see introduction) + +

7: Noise reduction enable

+ +Enable or disable noise reduction. When disabled it just passes audio through + +

8: Audio mute and device selection

+ + - Left click: Mute or unmute audio + - Right click: opens a dialog to select audio output device + +

9: Input volume

+ +This button lets you adjust the input volume. Adjust for best dynamic but the peaks should not exceed 100% as displayed in the VU meter next (10) + +

10: Input VU meter

+ +This is the VU meter of the audio entering the noise reduction block. The peaks should not exceed 100% + +

11: Record audio output

+ +Start/stop recording. Each start -> stop creates a new record file (see next) + +

12: Select output record file

+ +Click on this icon to open a file selection dialog that lets you specify the location and name of the output files. + +Each recording is written in a new file with the starting timestamp before the `.wav` extension in `yyyy-MM-ddTHH_mm_ss_zzz` format. It keeps the first dot limited groups of the filename before the `.wav` extension if there are two such groups or before the two last groups if there are more than two groups. Examples: + + - Given file name: `test.wav` then a recording file will be like: `test.2020-08-05T21_39_07_974.wav` + - Given file name: `test.2020-08-05T20_36_15_974.wav` then a recording file will be like (with timestamp updated): `test.2020-08-05T21_41_21_173.wav` + - Given file name: `test.first.wav` then a recording file will be like: `test.2020-08-05T22_00_07_974.wav` + - Given file name: `record.test.first.wav` then a recording file will be like: `record.test.2020-08-05T21_39_52_974.wav` + +If a filename is given without `.wav` extension then the `.wav` extension is appended automatically before the above algorithm is applied. If a filename is given with an extension different of `.wav` then the extension is replaced by `.wav` automatically before the above algorithm is applied. + +The file path currently being written (or last closed) appears at the right of the button (13). + +

13: Output record file name

+ +File path currently being written (or last closed) diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 8504b3d9c..e995e1a89 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -5646,6 +5646,18 @@ margin-bottom: 20px; "type" : "integer", "description" : "Denoiser type\n * 0 - none\n * 1 - RNnoise\n" }, + "enableDenoiser" : { + "type" : "integer", + "description" : "Enable denoiser\n * 1 - enable\n * 0 - disable\n" + }, + "volumeTenths" : { + "type" : "integer", + "description" : "Output volume in tenths (e.g., 10 = 1.0)\n" + }, + "audioDeviceName" : { + "type" : "string", + "description" : "Audio output device name" + }, "audioMute" : { "type" : "integer", "description" : "Audio mute\n * 1 - mute\n * 0 - unmute\n" @@ -59720,7 +59732,7 @@ except ApiException as e:
- Generated 2026-01-06T07:31:33.605+01:00 + Generated 2026-01-10T11:16:10.140+01:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/Denoiser.yaml b/sdrbase/resources/webapi/doc/swagger/include/Denoiser.yaml index d0da09d97..3908b2c41 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/Denoiser.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/Denoiser.yaml @@ -7,6 +7,19 @@ DenoiserSettings: Denoiser type * 0 - none * 1 - RNnoise + enableDenoiser: + type: integer + description: > + Enable denoiser + * 1 - enable + * 0 - disable + volumeTenths: + type: integer + description: > + Output volume in tenths (e.g., 10 = 1.0) + audioDeviceName: + type: string + description: Audio output device name audioMute: type: integer description: > diff --git a/swagger/sdrangel/api/swagger/include/Denoiser.yaml b/swagger/sdrangel/api/swagger/include/Denoiser.yaml index 8739e4c56..dbc911e00 100644 --- a/swagger/sdrangel/api/swagger/include/Denoiser.yaml +++ b/swagger/sdrangel/api/swagger/include/Denoiser.yaml @@ -7,6 +7,19 @@ DenoiserSettings: Denoiser type * 0 - none * 1 - RNnoise + enableDenoiser: + type: integer + description: > + Enable denoiser + * 1 - enable + * 0 - disable + volumeTenths: + type: integer + description: > + Output volume in tenths (e.g., 10 = 1.0) + audioDeviceName: + type: string + description: Audio output device name audioMute: type: integer description: > diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index 8504b3d9c..e995e1a89 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -5646,6 +5646,18 @@ margin-bottom: 20px; "type" : "integer", "description" : "Denoiser type\n * 0 - none\n * 1 - RNnoise\n" }, + "enableDenoiser" : { + "type" : "integer", + "description" : "Enable denoiser\n * 1 - enable\n * 0 - disable\n" + }, + "volumeTenths" : { + "type" : "integer", + "description" : "Output volume in tenths (e.g., 10 = 1.0)\n" + }, + "audioDeviceName" : { + "type" : "string", + "description" : "Audio output device name" + }, "audioMute" : { "type" : "integer", "description" : "Audio mute\n * 1 - mute\n * 0 - unmute\n" @@ -59720,7 +59732,7 @@ except ApiException as e:
- Generated 2026-01-06T07:31:33.605+01:00 + Generated 2026-01-10T11:16:10.140+01:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.cpp index dfcdf27c3..0c154a9d2 100644 --- a/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.cpp @@ -30,6 +30,12 @@ SWGDenoiserSettings::SWGDenoiserSettings(QString* json) { SWGDenoiserSettings::SWGDenoiserSettings() { denoiser_type = 0; m_denoiser_type_isSet = false; + enable_denoiser = 0; + m_enable_denoiser_isSet = false; + volume_tenths = 0; + m_volume_tenths_isSet = false; + audio_device_name = nullptr; + m_audio_device_name_isSet = false; audio_mute = 0; m_audio_mute_isSet = false; title = nullptr; @@ -62,6 +68,12 @@ void SWGDenoiserSettings::init() { denoiser_type = 0; m_denoiser_type_isSet = false; + enable_denoiser = 0; + m_enable_denoiser_isSet = false; + volume_tenths = 0; + m_volume_tenths_isSet = false; + audio_device_name = new QString(""); + m_audio_device_name_isSet = false; audio_mute = 0; m_audio_mute_isSet = false; title = new QString(""); @@ -90,6 +102,11 @@ void SWGDenoiserSettings::cleanup() { + + if(audio_device_name != nullptr) { + delete audio_device_name; + } + if(title != nullptr) { delete title; } @@ -123,6 +140,12 @@ void SWGDenoiserSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&denoiser_type, pJson["denoiserType"], "qint32", ""); + ::SWGSDRangel::setValue(&enable_denoiser, pJson["enableDenoiser"], "qint32", ""); + + ::SWGSDRangel::setValue(&volume_tenths, pJson["volumeTenths"], "qint32", ""); + + ::SWGSDRangel::setValue(&audio_device_name, pJson["audioDeviceName"], "QString", "QString"); + ::SWGSDRangel::setValue(&audio_mute, pJson["audioMute"], "qint32", ""); ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); @@ -164,6 +187,15 @@ SWGDenoiserSettings::asJsonObject() { if(m_denoiser_type_isSet){ obj->insert("denoiserType", QJsonValue(denoiser_type)); } + if(m_enable_denoiser_isSet){ + obj->insert("enableDenoiser", QJsonValue(enable_denoiser)); + } + if(m_volume_tenths_isSet){ + obj->insert("volumeTenths", QJsonValue(volume_tenths)); + } + if(audio_device_name != nullptr && *audio_device_name != QString("")){ + toJsonValue(QString("audioDeviceName"), audio_device_name, obj, QString("QString")); + } if(m_audio_mute_isSet){ obj->insert("audioMute", QJsonValue(audio_mute)); } @@ -211,6 +243,36 @@ SWGDenoiserSettings::setDenoiserType(qint32 denoiser_type) { this->m_denoiser_type_isSet = true; } +qint32 +SWGDenoiserSettings::getEnableDenoiser() { + return enable_denoiser; +} +void +SWGDenoiserSettings::setEnableDenoiser(qint32 enable_denoiser) { + this->enable_denoiser = enable_denoiser; + this->m_enable_denoiser_isSet = true; +} + +qint32 +SWGDenoiserSettings::getVolumeTenths() { + return volume_tenths; +} +void +SWGDenoiserSettings::setVolumeTenths(qint32 volume_tenths) { + this->volume_tenths = volume_tenths; + this->m_volume_tenths_isSet = true; +} + +QString* +SWGDenoiserSettings::getAudioDeviceName() { + return audio_device_name; +} +void +SWGDenoiserSettings::setAudioDeviceName(QString* audio_device_name) { + this->audio_device_name = audio_device_name; + this->m_audio_device_name_isSet = true; +} + qint32 SWGDenoiserSettings::getAudioMute() { return audio_mute; @@ -329,6 +391,15 @@ SWGDenoiserSettings::isSet(){ if(m_denoiser_type_isSet){ isObjectUpdated = true; break; } + if(m_enable_denoiser_isSet){ + isObjectUpdated = true; break; + } + if(m_volume_tenths_isSet){ + isObjectUpdated = true; break; + } + if(audio_device_name && *audio_device_name != QString("")){ + isObjectUpdated = true; break; + } if(m_audio_mute_isSet){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.h b/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.h index e8d13a618..9fa837426 100644 --- a/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGDenoiserSettings.h @@ -46,6 +46,15 @@ public: qint32 getDenoiserType(); void setDenoiserType(qint32 denoiser_type); + qint32 getEnableDenoiser(); + void setEnableDenoiser(qint32 enable_denoiser); + + qint32 getVolumeTenths(); + void setVolumeTenths(qint32 volume_tenths); + + QString* getAudioDeviceName(); + void setAudioDeviceName(QString* audio_device_name); + qint32 getAudioMute(); void setAudioMute(qint32 audio_mute); @@ -86,6 +95,15 @@ private: qint32 denoiser_type; bool m_denoiser_type_isSet; + qint32 enable_denoiser; + bool m_enable_denoiser_isSet; + + qint32 volume_tenths; + bool m_volume_tenths_isSet; + + QString* audio_device_name; + bool m_audio_device_name_isSet; + qint32 audio_mute; bool m_audio_mute_isSet;