From 599ee2651e0f0753ff9d9013f50c9723a3b256dd Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 9 Nov 2020 15:52:25 +0000 Subject: [PATCH 1/6] Allow volume to be set after start() --- sdrbase/audio/audioinput.cpp | 6 ++++++ sdrbase/audio/audioinput.h | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sdrbase/audio/audioinput.cpp b/sdrbase/audio/audioinput.cpp index 261e9a440..1c8db7838 100644 --- a/sdrbase/audio/audioinput.cpp +++ b/sdrbase/audio/audioinput.cpp @@ -189,3 +189,9 @@ qint64 AudioInput::writeData(const char *data, qint64 len) return len; } +void AudioInput::setVolume(float volume) +{ + m_volume = volume; + if (m_audioInput != nullptr) + m_audioInput->setVolume(m_volume); +} diff --git a/sdrbase/audio/audioinput.h b/sdrbase/audio/audioinput.h index 9b086791b..78f706edc 100644 --- a/sdrbase/audio/audioinput.h +++ b/sdrbase/audio/audioinput.h @@ -44,7 +44,7 @@ public: uint getRate() const { return m_audioFormat.sampleRate(); } void setOnExit(bool onExit) { m_onExit = onExit; } - void setVolume(float volume) { m_volume = volume; } + void setVolume(float volume); private: QMutex m_mutex; From 00e5a4de3488d0dcce2c6adfdc5b3200d508c1d2 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 9 Nov 2020 15:54:35 +0000 Subject: [PATCH 2/6] Add audio sample source plugin --- doc/img/AudioInput_plugin.png | Bin 0 -> 23287 bytes doc/img/AudioInput_plugin.xcf | Bin 0 -> 44111 bytes plugins/samplesource/CMakeLists.txt | 1 + .../samplesource/audioinput/CMakeLists.txt | 57 ++ .../samplesource/audioinput/audioinput.cpp | 519 ++++++++++++++++++ plugins/samplesource/audioinput/audioinput.h | 161 ++++++ .../samplesource/audioinput/audioinputgui.cpp | 304 ++++++++++ .../samplesource/audioinput/audioinputgui.h | 83 +++ .../samplesource/audioinput/audioinputgui.ui | 391 +++++++++++++ .../audioinput/audioinputplugin.cpp | 151 +++++ .../audioinput/audioinputplugin.h | 56 ++ .../audioinput/audioinputsettings.cpp | 99 ++++ .../audioinput/audioinputsettings.h | 64 +++ .../audioinput/audioinputthread.cpp | 141 +++++ .../audioinput/audioinputthread.h | 63 +++ .../audioinput/audioinputwebapiadapter.cpp | 49 ++ .../audioinput/audioinputwebapiadapter.h | 42 ++ plugins/samplesource/audioinput/readme.md | 46 ++ sdrbase/resources/webapi/doc/html2/index.html | 43 +- sdrbase/webapi/webapirequestmapper.cpp | 6 + sdrbase/webapi/webapiutils.cpp | 3 + .../api/swagger/include/AudioInput.yaml | 32 ++ .../api/swagger/include/DeviceSettings.yaml | 2 + swagger/sdrangel/code/html2/index.html | 43 +- .../code/qt5/client/SWGAudioInputSettings.cpp | 296 ++++++++++ .../code/qt5/client/SWGAudioInputSettings.h | 107 ++++ .../code/qt5/client/SWGDeviceSettings.cpp | 25 + .../code/qt5/client/SWGDeviceSettings.h | 7 + .../code/qt5/client/SWGModelFactory.h | 4 + 29 files changed, 2793 insertions(+), 2 deletions(-) create mode 100644 doc/img/AudioInput_plugin.png create mode 100644 doc/img/AudioInput_plugin.xcf create mode 100644 plugins/samplesource/audioinput/CMakeLists.txt create mode 100644 plugins/samplesource/audioinput/audioinput.cpp create mode 100644 plugins/samplesource/audioinput/audioinput.h create mode 100644 plugins/samplesource/audioinput/audioinputgui.cpp create mode 100644 plugins/samplesource/audioinput/audioinputgui.h create mode 100644 plugins/samplesource/audioinput/audioinputgui.ui create mode 100644 plugins/samplesource/audioinput/audioinputplugin.cpp create mode 100644 plugins/samplesource/audioinput/audioinputplugin.h create mode 100644 plugins/samplesource/audioinput/audioinputsettings.cpp create mode 100644 plugins/samplesource/audioinput/audioinputsettings.h create mode 100644 plugins/samplesource/audioinput/audioinputthread.cpp create mode 100644 plugins/samplesource/audioinput/audioinputthread.h create mode 100644 plugins/samplesource/audioinput/audioinputwebapiadapter.cpp create mode 100644 plugins/samplesource/audioinput/audioinputwebapiadapter.h create mode 100644 plugins/samplesource/audioinput/readme.md create mode 100644 swagger/sdrangel/api/swagger/include/AudioInput.yaml create mode 100644 swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.h diff --git a/doc/img/AudioInput_plugin.png b/doc/img/AudioInput_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..2a875dad337884ae9dc4304be82f75d1971d17aa GIT binary patch literal 23287 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 unWlR5ck?Ia@!35|Q>*p literal 0 HcmV?d00001 diff --git a/plugins/samplesource/CMakeLists.txt b/plugins/samplesource/CMakeLists.txt index c86941e2d..29e40bb00 100644 --- a/plugins/samplesource/CMakeLists.txt +++ b/plugins/samplesource/CMakeLists.txt @@ -62,4 +62,5 @@ if(ENABLE_USRP AND UHD_FOUND) add_subdirectory(usrpinput) endif() +add_subdirectory(audioinput) add_subdirectory(kiwisdr) \ No newline at end of file diff --git a/plugins/samplesource/audioinput/CMakeLists.txt b/plugins/samplesource/audioinput/CMakeLists.txt new file mode 100644 index 000000000..13233fdfc --- /dev/null +++ b/plugins/samplesource/audioinput/CMakeLists.txt @@ -0,0 +1,57 @@ +project(audioinput) + +set(audioinput_SOURCES + audioinput.cpp + audioinputplugin.cpp + audioinputsettings.cpp + audioinputwebapiadapter.cpp + audioinputthread.cpp +) + +set(audioinput_HEADERS + audioinput.h + audioinputplugin.h + audioinputsettings.h + audioinputwebapiadapter.h + audioinputthread.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(audioinput_SOURCES + ${audioinput_SOURCES} + audioinputgui.cpp + audioinputgui.ui + ) + set(audioinput_HEADERS + ${audioinput_HEADERS} + audioinputgui.h + ) + + set(TARGET_NAME inputaudio) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME inputaudiosrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${audioinput_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/samplesource/audioinput/audioinput.cpp b/plugins/samplesource/audioinput/audioinput.cpp new file mode 100644 index 000000000..e03173627 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinput.cpp @@ -0,0 +1,519 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include +#include +#include + +#include "SWGDeviceSettings.h" +#include "SWGDeviceState.h" + +#include "dsp/dspcommands.h" +#include "dsp/dspengine.h" +#include "device/deviceapi.h" + +#include "audioinput.h" +#include "audioinputthread.h" + +MESSAGE_CLASS_DEFINITION(AudioInputSource::AudioInput::MsgConfigureAudioInput, Message) +MESSAGE_CLASS_DEFINITION(AudioInputSource::AudioInput::MsgStartStop, Message) + +namespace AudioInputSource { + +AudioInput::AudioInput(DeviceAPI *deviceAPI) : + m_deviceAPI(deviceAPI), + m_settings(), + m_thread(nullptr), + m_deviceDescription("AudioInput"), + m_running(false) +{ + m_fifo.setSize(20*AudioInputThread::m_convBufSamples); + openDevice(); + m_deviceAPI->setNbSourceStreams(1); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +AudioInput::~AudioInput() +{ + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + + if (m_running) { + stop(); + } + + closeDevice(); +} + +void AudioInput::destroy() +{ + delete this; +} + +bool AudioInput::openDevice() +{ + if (!openAudioDevice(m_settings.m_deviceName, m_settings.m_sampleRate)) + { + qCritical("AudioInput::openDevice: could not open audio source"); + return false; + } + else + return true; +} + +bool AudioInput::openAudioDevice(QString deviceName, qint32 sampleRate) +{ + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + const QList& audioList = audioDeviceManager->getInputDevices(); + + for (const auto &itAudio : audioList) + { + if (AudioInputSettings::getFullDeviceName(itAudio) == deviceName) + { + // FIXME: getInputDeviceIndex needs a realm parameter (itAudio.realm()) - need to look on Linux to see if it uses default? + int deviceIndex = audioDeviceManager->getInputDeviceIndex(itAudio.deviceName()); + m_audioInput.start(deviceIndex, sampleRate); + m_audioInput.addFifo(&m_fifo); + return true; + } + } + return false; +} + +void AudioInput::init() +{ + applySettings(m_settings, true); +} + +bool AudioInput::start() +{ + qDebug() << "AudioInput::start"; + + if (m_running) stop(); + + if(!m_sampleFifo.setSize(96000*4)) + { + qCritical("Could not allocate SampleFifo"); + return false; + } + + m_thread = new AudioInputThread(&m_sampleFifo, &m_fifo); + m_thread->setLog2Decimation(m_settings.m_log2Decim); + m_thread->setIQMapping(m_settings.m_iqMapping); + m_thread->startWork(); + + applySettings(m_settings, true); + + qDebug("AudioInput::started"); + m_running = true; + + return true; +} + +void AudioInput::closeDevice() +{ + m_audioInput.removeFifo(&m_fifo); + m_audioInput.stop(); +} + +void AudioInput::stop() +{ + if (m_thread) + { + m_thread->stopWork(); + // wait for thread to quit ? + delete m_thread; + m_thread = nullptr; + } + + m_running = false; +} + +QByteArray AudioInput::serialize() const +{ + return m_settings.serialize(); +} + +bool AudioInput::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureAudioInput* message = MsgConfigureAudioInput::create(m_settings, true); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) + { + MsgConfigureAudioInput* messageToGUI = MsgConfigureAudioInput::create(m_settings, true); + m_guiMessageQueue->push(messageToGUI); + } + + return success; +} + +const QString& AudioInput::getDeviceDescription() const +{ + return m_deviceDescription; +} + +int AudioInput::getSampleRate() const +{ + return m_settings.m_sampleRate/(1<initDeviceEngine()) + { + m_deviceAPI->startDeviceEngine(); + } + } + else + { + m_deviceAPI->stopDeviceEngine(); + } + + if (m_settings.m_useReverseAPI) { + webapiReverseSendStartStop(cmd.getStartStop()); + } + + return true; + } + else + { + return false; + } +} + +void AudioInput::applySettings(const AudioInputSettings& settings, bool force) +{ + bool forwardChange = false; + QList reverseAPIKeys; + + if ((m_settings.m_deviceName != settings.m_deviceName) + || (m_settings.m_sampleRate != settings.m_sampleRate) || force) + { + closeDevice(); + if (openAudioDevice(settings.m_deviceName, settings.m_sampleRate)) + qDebug() << "AudioInput::applySettings: opened device " << settings.m_deviceName << " with sample rate " << m_audioInput.getRate(); + else + qCritical() << "AudioInput::applySettings: failed to open device " << settings.m_deviceName; + } + + if ((m_settings.m_deviceName != settings.m_deviceName) || force) + { + //reverseAPIKeys.append("device"); + } + + if ((m_settings.m_sampleRate != settings.m_sampleRate) || force) + { + //reverseAPIKeys.append("sampleRate"); + forwardChange = true; + } + + if ((m_settings.m_volume != settings.m_volume) || force) + { + //reverseAPIKeys.append("volume"); + m_audioInput.setVolume(settings.m_volume); + qDebug() << "AudioInput::applySettings: set volume to " << settings.m_volume; + } + + if ((m_settings.m_log2Decim != settings.m_log2Decim) || force) + { + reverseAPIKeys.append("log2Decim"); + forwardChange = true; + + if (m_thread) + { + m_thread->setLog2Decimation(settings.m_log2Decim); + qDebug() << "AudioInput::applySettings: set decimation to " << (1<setIQMapping(settings.m_iqMapping); + } + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; + + if (forwardChange) + { + DSPSignalNotification *notif = new DSPSignalNotification(m_settings.m_sampleRate/(1<getDeviceEngineInputMessageQueue()->push(notif); + } +} + +int AudioInput::webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + return 200; +} + +int AudioInput::webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + MsgStartStop *message = MsgStartStop::create(run); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgStartStop *msgToGUI = MsgStartStop::create(run); + m_guiMessageQueue->push(msgToGUI); + } + + return 200; +} + +int AudioInput::webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAudioInputSettings(new SWGSDRangel::SWGAudioInputSettings()); + response.getAudioInputSettings()->init(); + webapiFormatDeviceSettings(response, m_settings); + + return 200; +} + +int AudioInput::webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage) +{ + (void) errorMessage; + AudioInputSettings settings = m_settings; + webapiUpdateDeviceSettings(settings, deviceSettingsKeys, response); + + MsgConfigureAudioInput *msg = MsgConfigureAudioInput::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAudioInput *msgToGUI = MsgConfigureAudioInput::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatDeviceSettings(response, settings); + return 200; +} + +void AudioInput::webapiUpdateDeviceSettings( + AudioInputSettings& settings, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response) +{ + if (deviceSettingsKeys.contains("device")) { + settings.m_deviceName = *response.getAudioInputSettings()->getDevice(); + } + if (deviceSettingsKeys.contains("devSampleRate")) { + settings.m_sampleRate = response.getAudioInputSettings()->getDevSampleRate(); + } + if (deviceSettingsKeys.contains("volume")) { + settings.m_volume = response.getAudioInputSettings()->getVolume(); + } + if (deviceSettingsKeys.contains("log2Decim")) { + settings.m_log2Decim = response.getAudioInputSettings()->getLog2Decim(); + } + if (deviceSettingsKeys.contains("iqMapping")) { + settings.m_iqMapping = (AudioInputSettings::IQMapping)response.getAudioInputSettings()->getIqMapping(); + } + if (deviceSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAudioInputSettings()->getUseReverseApi() != 0; + } + if (deviceSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAudioInputSettings()->getReverseApiAddress(); + } + if (deviceSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAudioInputSettings()->getReverseApiPort(); + } + if (deviceSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getAudioInputSettings()->getReverseApiDeviceIndex(); + } +} + +void AudioInput::webapiFormatDeviceSettings(SWGSDRangel::SWGDeviceSettings& response, const AudioInputSettings& settings) +{ + response.getAudioInputSettings()->setDevice(new QString(settings.m_deviceName)); + response.getAudioInputSettings()->setDevSampleRate(settings.m_sampleRate); + response.getAudioInputSettings()->setVolume(settings.m_volume); + response.getAudioInputSettings()->setLog2Decim(settings.m_log2Decim); + response.getAudioInputSettings()->setIqMapping((int)settings.m_iqMapping); + + response.getAudioInputSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAudioInputSettings()->getReverseApiAddress()) { + *response.getAudioInputSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAudioInputSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAudioInputSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAudioInputSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); +} + +void AudioInput::webapiReverseSendSettings(QList& deviceSettingsKeys, const AudioInputSettings& settings, bool force) +{ + SWGSDRangel::SWGDeviceSettings *swgDeviceSettings = new SWGSDRangel::SWGDeviceSettings(); + swgDeviceSettings->setDirection(0); // single Rx + swgDeviceSettings->setOriginatorIndex(m_deviceAPI->getDeviceSetIndex()); + swgDeviceSettings->setDeviceHwType(new QString("AudioInput")); + swgDeviceSettings->setAudioInputSettings(new SWGSDRangel::SWGAudioInputSettings()); + SWGSDRangel::SWGAudioInputSettings *swgAudioInputSettings = swgDeviceSettings->getAudioInputSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (deviceSettingsKeys.contains("device") || force) { + swgAudioInputSettings->setDevice(new QString(settings.m_deviceName)); + } + if (deviceSettingsKeys.contains("devSampleRate") || force) { + swgAudioInputSettings->setDevSampleRate(settings.m_sampleRate); + } + if (deviceSettingsKeys.contains("volume") || force) { + swgAudioInputSettings->setVolume(settings.m_volume); + } + if (deviceSettingsKeys.contains("log2Decim") || force) { + swgAudioInputSettings->setLog2Decim(settings.m_log2Decim); + } + if (deviceSettingsKeys.contains("iqMapping") || force) { + swgAudioInputSettings->setIqMapping(settings.m_iqMapping); + } + + QString deviceSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/device/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex); + m_networkRequest.setUrl(QUrl(deviceSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgDeviceSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgDeviceSettings; +} + +void AudioInput::webapiReverseSendStartStop(bool start) +{ + SWGSDRangel::SWGDeviceSettings *swgDeviceSettings = new SWGSDRangel::SWGDeviceSettings(); + swgDeviceSettings->setDirection(0); // single Rx + swgDeviceSettings->setOriginatorIndex(m_deviceAPI->getDeviceSetIndex()); + swgDeviceSettings->setDeviceHwType(new QString("AudioInput")); + + QString deviceSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/device/run") + .arg(m_settings.m_reverseAPIAddress) + .arg(m_settings.m_reverseAPIPort) + .arg(m_settings.m_reverseAPIDeviceIndex); + m_networkRequest.setUrl(QUrl(deviceSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgDeviceSettings->asJson().toUtf8()); + buffer->seek(0); + QNetworkReply *reply; + + if (start) { + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + } else { + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + } + + buffer->setParent(reply); + delete swgDeviceSettings; +} + +void AudioInput::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "AudioInput::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AudioInput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +} diff --git a/plugins/samplesource/audioinput/audioinput.h b/plugins/samplesource/audioinput/audioinput.h new file mode 100644 index 000000000..2632acbfa --- /dev/null +++ b/plugins/samplesource/audioinput/audioinput.h @@ -0,0 +1,161 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOINPUT_H +#define INCLUDE_AUDIOINPUT_H + +#include + +#include +#include +#include + +#include "dsp/devicesamplesource.h" +#include "audio/audioinput.h" +#include "audio/audiofifo.h" + +#include "audioinputsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class DeviceAPI; +class AudioInputThread; + +// AudioInput is used in sdrbase/audio/audioinput.h +namespace AudioInputSource { + +class AudioInput : public DeviceSampleSource { + Q_OBJECT +public: + class MsgConfigureAudioInput : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AudioInputSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAudioInput* create(const AudioInputSettings& settings, bool force) + { + return new MsgConfigureAudioInput(settings, force); + } + + private: + AudioInputSettings m_settings; + bool m_force; + + MsgConfigureAudioInput(const AudioInputSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgStartStop : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgStartStop* create(bool startStop) { + return new MsgStartStop(startStop); + } + + protected: + bool m_startStop; + + MsgStartStop(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + AudioInput(DeviceAPI *deviceAPI); + virtual ~AudioInput(); + virtual void destroy(); + + virtual void init(); + virtual bool start(); + virtual void stop(); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual void setMessageQueueToGUI(MessageQueue *queue) { m_guiMessageQueue = queue; } + virtual const QString& getDeviceDescription() const; + virtual int getSampleRate() const; + virtual void setSampleRate(int sampleRate) { (void) sampleRate; } + virtual quint64 getCenterFrequency() const; + virtual void setCenterFrequency(qint64 centerFrequency); + + virtual bool handleMessage(const Message& message); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage); + + virtual int webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + static void webapiFormatDeviceSettings( + SWGSDRangel::SWGDeviceSettings& response, + const AudioInputSettings& settings); + + static void webapiUpdateDeviceSettings( + AudioInputSettings& settings, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response); + +private: + DeviceAPI *m_deviceAPI; + ::AudioInput m_audioInput; + AudioFifo m_fifo; + QMutex m_mutex; + AudioInputSettings m_settings; + AudioInputThread* m_thread; + QString m_deviceDescription; + bool m_running; + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + bool openDevice(); + void closeDevice(); + bool openAudioDevice(QString deviceName, int sampleRate); + void applySettings(const AudioInputSettings& settings, bool force); + + void webapiReverseSendSettings(QList& deviceSettingsKeys, const AudioInputSettings& settings, bool force); + void webapiReverseSendStartStop(bool start); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +} + +#endif // INCLUDE_AUDIOINPUT_H diff --git a/plugins/samplesource/audioinput/audioinputgui.cpp b/plugins/samplesource/audioinput/audioinputgui.cpp new file mode 100644 index 000000000..32fdac22c --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputgui.cpp @@ -0,0 +1,304 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "ui_audioinputgui.h" +#include "gui/colormapper.h" +#include "gui/glspectrum.h" +#include "gui/crightclickenabler.h" +#include "gui/basicdevicesettingsdialog.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "audioinputgui.h" + +#include "device/deviceapi.h" +#include "device/deviceuiset.h" + +AudioInputGui::AudioInputGui(DeviceUISet *deviceUISet, QWidget* parent) : + DeviceGUI(parent), + ui(new Ui::AudioInputGui), + m_deviceUISet(deviceUISet), + m_forceSettings(true), + m_settings(), + m_sampleSource(NULL) +{ + m_sampleSource = (AudioInputSource::AudioInput*) m_deviceUISet->m_deviceAPI->getSampleSource(); + + ui->setupUi(this); + + connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateHardware())); + + CRightClickEnabler *startStopRightClickEnabler = new CRightClickEnabler(ui->startStop); + connect(startStopRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(openDeviceSettingsDialog(const QPoint &))); + + displaySettings(); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()), Qt::QueuedConnection); + m_sampleSource->setMessageQueueToGUI(&m_inputMessageQueue); +} + +AudioInputGui::~AudioInputGui() +{ + delete ui; +} + +void AudioInputGui::destroy() +{ + delete this; +} + +void AudioInputGui::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + sendSettings(); +} + +QByteArray AudioInputGui::serialize() const +{ + return m_settings.serialize(); +} + +bool AudioInputGui::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) + { + displaySettings(); + m_forceSettings = true; + sendSettings(); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool AudioInputGui::handleMessage(const Message& message) +{ + if (AudioInputSource::AudioInput::MsgConfigureAudioInput::match(message)) + { + const AudioInputSource::AudioInput::MsgConfigureAudioInput& cfg = (AudioInputSource::AudioInput::MsgConfigureAudioInput&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (AudioInputSource::AudioInput::MsgStartStop::match(message)) + { + AudioInputSource::AudioInput::MsgStartStop& notif = (AudioInputSource::AudioInput::MsgStartStop&) message; + blockApplySettings(true); + ui->startStop->setChecked(notif.getStartStop()); + blockApplySettings(false); + + return true; + } + else + { + return false; + } +} + +void AudioInputGui::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != 0) + { + qDebug("AudioInputGui::handleInputMessages: message: %s", message->getIdentifier()); + + if (DSPSignalNotification::match(*message)) + { + DSPSignalNotification* notif = (DSPSignalNotification*) message; + m_sampleRate = notif->getSampleRate(); + qDebug("AudioInputGui::handleInputMessages: DSPSignalNotification: SampleRate: %d", notif->getSampleRate()); + updateSampleRateAndFrequency(); + + delete message; + } + else + { + if (handleMessage(*message)) + { + delete message; + } + } + } +} + +void AudioInputGui::updateSampleRateAndFrequency() +{ +/* + // Can't seem to get main spectrum to only display real part for mono/I-channel only + if (m_settings.m_iqMapping <= AudioInputSettings::IQMapping::R) + { + m_deviceUISet->getSpectrum()->setSampleRate(m_sampleRate / 2); + m_deviceUISet->getSpectrum()->setCenterFrequency(m_sampleRate / 4); + m_deviceUISet->getSpectrum()->setSsbSpectrum(true); + m_deviceUISet->getSpectrum()->setLsbDisplay(false); + } + else +*/ + { + m_deviceUISet->getSpectrum()->setSampleRate(m_sampleRate); + m_deviceUISet->getSpectrum()->setCenterFrequency(0); + m_deviceUISet->getSpectrum()->setSsbSpectrum(false); + m_deviceUISet->getSpectrum()->setLsbDisplay(false); + } + ui->deviceRateText->setText(tr("%1k").arg((float)m_sampleRate / 1000)); +} + +void AudioInputGui::refreshDeviceList() +{ + ui->device->blockSignals(true); + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + const QList& audioList = audioDeviceManager->getInputDevices(); + + ui->device->clear(); + for (const auto &itAudio : audioList) + { + ui->device->addItem(AudioInputSettings::getFullDeviceName(itAudio)); + } + ui->device->blockSignals(false); +} + +void AudioInputGui::refreshSampleRates(QString deviceName) +{ + ui->sampleRate->blockSignals(true); + ui->sampleRate->clear(); + const auto deviceInfos = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); + for (const QAudioDeviceInfo &deviceInfo : deviceInfos) + { + if (deviceName == AudioInputSettings::getFullDeviceName(deviceInfo)) + { + QList sampleRates = deviceInfo.supportedSampleRates(); + for(int i = 0; i < sampleRates.size(); ++i) + { + ui->sampleRate->addItem(QString("%1").arg(sampleRates[i])); + } + } + } + ui->sampleRate->blockSignals(false); + + int index = ui->sampleRate->findText(QString("%1").arg(m_settings.m_sampleRate)); + if (index >= 0) + ui->sampleRate->setCurrentIndex(index); + else + ui->sampleRate->setCurrentIndex(0); +} + +void AudioInputGui::displaySettings() +{ + refreshDeviceList(); + int index = ui->device->findText(m_settings.m_deviceName); + if (index >= 0) + ui->device->setCurrentIndex(index); + else + ui->device->setCurrentIndex(0); + ui->decim->setCurrentIndex(m_settings.m_log2Decim); + ui->volume->setValue((int)(m_settings.m_volume*10.0f)); + ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 3, 'f', 1)); + ui->channels->setCurrentIndex((int)m_settings.m_iqMapping); + refreshSampleRates(ui->device->currentText()); +} + +void AudioInputGui::on_device_currentIndexChanged(int index) +{ + m_settings.m_deviceName = ui->device->currentText(); + refreshSampleRates(m_settings.m_deviceName); + sendSettings(); +} + +void AudioInputGui::on_sampleRate_currentIndexChanged(int index) +{ + m_settings.m_sampleRate = ui->sampleRate->currentText().toInt(); + sendSettings(); +} + +void AudioInputGui::on_decim_currentIndexChanged(int index) +{ + if ((index < 0) || (index > 6)) { + return; + } + + m_settings.m_log2Decim = index; + sendSettings(); +} + +void AudioInputGui::on_volume_valueChanged(int value) +{ + m_settings.m_volume = value/10.0f; + ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 3, 'f', 1)); + sendSettings(); +} + +void AudioInputGui::on_channels_currentIndexChanged(int index) +{ + m_settings.m_iqMapping = (AudioInputSettings::IQMapping)index; + updateSampleRateAndFrequency(); + sendSettings(); +} + +void AudioInputGui::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + AudioInputSource::AudioInput::MsgStartStop *message = AudioInputSource::AudioInput::MsgStartStop::create(checked); + m_sampleSource->getInputMessageQueue()->push(message); + } +} + +void AudioInputGui::sendSettings() +{ + if(!m_updateTimer.isActive()) + m_updateTimer.start(100); +} + +void AudioInputGui::updateHardware() +{ + if (m_doApplySettings) + { + AudioInputSource::AudioInput::MsgConfigureAudioInput* message = AudioInputSource::AudioInput::MsgConfigureAudioInput::create(m_settings, m_forceSettings); + m_sampleSource->getInputMessageQueue()->push(message); + m_forceSettings = false; + m_updateTimer.stop(); + } +} + +void AudioInputGui::openDeviceSettingsDialog(const QPoint& p) +{ + BasicDeviceSettingsDialog dialog(this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + + dialog.move(p); + dialog.exec(); + + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + + sendSettings(); +} diff --git a/plugins/samplesource/audioinput/audioinputgui.h b/plugins/samplesource/audioinput/audioinputgui.h new file mode 100644 index 000000000..8c8507f77 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputgui.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOINPUTGUI_H +#define INCLUDE_AUDIOINPUTGUI_H + +#include +#include +#include + +#include "util/messagequeue.h" + +#include "audioinput.h" + +class QWidget; +class DeviceUISet; + +namespace Ui { + class AudioInputGui; +} + +class AudioInputGui : public DeviceGUI { + Q_OBJECT + +public: + explicit AudioInputGui(DeviceUISet *deviceUISet, QWidget* parent = 0); + virtual ~AudioInputGui(); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + Ui::AudioInputGui* ui; + + DeviceUISet* m_deviceUISet; + bool m_doApplySettings; + bool m_forceSettings; + AudioInputSettings m_settings; + QTimer m_updateTimer; + DeviceSampleSource* m_sampleSource; + int m_sampleRate; + + MessageQueue m_inputMessageQueue; + + void blockApplySettings(bool block) { m_doApplySettings = !block; } + void refreshDeviceList(); + void refreshSampleRates(QString deviceName); + void displaySettings(); + void sendSettings(); + void updateSampleRateAndFrequency(); + bool handleMessage(const Message& message); + +private slots: + void handleInputMessages(); + void on_device_currentIndexChanged(int index); + void on_sampleRate_currentIndexChanged(int index); + void on_decim_currentIndexChanged(int index); + void on_volume_valueChanged(int value); + void on_channels_currentIndexChanged(int index); + void on_startStop_toggled(bool checked); + void updateHardware(); + void openDeviceSettingsDialog(const QPoint& p); +}; + +#endif // INCLUDE_AUDIOINPUTGUI_H diff --git a/plugins/samplesource/audioinput/audioinputgui.ui b/plugins/samplesource/audioinput/audioinputgui.ui new file mode 100644 index 000000000..5c3511e66 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputgui.ui @@ -0,0 +1,391 @@ + + + AudioInputGui + + + + 0 + 0 + 320 + 350 + + + + + 0 + 0 + + + + + 320 + 350 + + + + + Liberation Sans + 9 + + + + FunCubeDongle + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 4 + + + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + + + + + Baseband I/Q sample rate kS/s + + + 00000k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Qt::Horizontal + + + + + + + + + + 0 + 0 + + + + Device + + + + + + + + 0 + 0 + + + + + 220 + 0 + + + + + + + + Refresh list of audio devices + + + ... + + + + :/recycle.png:/recycle.png + + + + + + + + + Qt::Horizontal + + + + + + + + + SR + + + + + + + + 100 + 0 + + + + Audio sample rate in Hz + + + + + + + Hz + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + 16777215 + + + + Decimation factor + + + + 1 + + + + + 2 + + + + + 4 + + + + + 8 + + + + + + + + Dec + + + + + + + + + Qt::Horizontal + + + + + + + + + Volume + + + + + + + + 24 + 0 + + + + + 24 + 24 + + + + Audio volume. Not supported by all devices + + + 10 + + + 10 + + + + + + + 1.0 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Channel Map + + + + + + + + 80 + 0 + + + + How audio channels map to IQ data + + + 0 + + + + I=L, Q=0 + + + + + I=R, Q=0 + + + + + I=L, Q=R + + + + + I=R, Q=L + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+
+ + + + +
diff --git a/plugins/samplesource/audioinput/audioinputplugin.cpp b/plugins/samplesource/audioinput/audioinputplugin.cpp new file mode 100644 index 000000000..d55faa3cb --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputplugin.cpp @@ -0,0 +1,151 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "audioinputplugin.h" +#include "audioinputwebapiadapter.h" + +#ifdef SERVER_MODE +#include "audioinput.h" +#else +#include "audioinputgui.h" +#endif + +const PluginDescriptor AudioInputPlugin::m_pluginDescriptor = { + QString("AudioInput"), + QString("Audio Input"), + QString("4.22.0"), + QString("(c) Jon Beniston, M7RCE and Edouard Griffiths, F4EXB"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +const QString AudioInputPlugin::m_hardwareID = "AudioInput"; +const QString AudioInputPlugin::m_deviceTypeID = AUDIOINPUT_DEVICE_TYPE_ID; + +AudioInputPlugin::AudioInputPlugin(QObject* parent) : + QObject(parent) +{ +} + +const PluginDescriptor& AudioInputPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void AudioInputPlugin::initPlugin(PluginAPI* pluginAPI) +{ + pluginAPI->registerSampleSource(m_deviceTypeID, this); +} + +void AudioInputPlugin::enumOriginDevices(QStringList& listedHwIds, OriginDevices& originDevices) +{ + if (listedHwIds.contains(m_hardwareID)) { // check if it was done + return; + } + + // We could list all input audio devices here separately + // but I thought it makes it simpler to switch between inputs + // if they are in the AudioInput GUI + originDevices.append(OriginDevice( + "Audio", + m_hardwareID, + "0", + 0, + 1, // nb Rx + 0 // nb Tx + )); +} + +PluginInterface::SamplingDevices AudioInputPlugin::enumSampleSources(const OriginDevices& originDevices) +{ + SamplingDevices result; + + for (OriginDevices::const_iterator it = originDevices.begin(); it != originDevices.end(); ++it) + { + if (it->hardwareId == m_hardwareID) + { + for (unsigned int j = 0; j < it->nbRxStreams; j++) + { + result.append(SamplingDevice( + it->displayableName, + it->hardwareId, + m_deviceTypeID, + it->serial, + it->sequence, + PluginInterface::SamplingDevice::PhysicalDevice, + PluginInterface::SamplingDevice::StreamSingleRx, + it->nbRxStreams, + j)); + } + } + } + + return result; +} + +#ifdef SERVER_MODE +DeviceGUI* AudioInputPlugin::createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + (void) sourceId; + (void) widget; + (void) deviceUISet; + return 0; +} +#else +DeviceGUI* AudioInputPlugin::createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + if(sourceId == m_deviceTypeID) + { + AudioInputGui* gui = new AudioInputGui(deviceUISet); + *widget = gui; + return gui; + } + else + { + return 0; + } +} +#endif + +DeviceSampleSource *AudioInputPlugin::createSampleSourcePluginInstance(const QString& sourceId, DeviceAPI *deviceAPI) +{ + if (sourceId == m_deviceTypeID) + { + AudioInputSource::AudioInput* input = new AudioInputSource::AudioInput(deviceAPI); + return input; + } + else + { + return 0; + } +} + +DeviceWebAPIAdapter *AudioInputPlugin::createDeviceWebAPIAdapter() const +{ + return new AudioInputWebAPIAdapter(); +} diff --git a/plugins/samplesource/audioinput/audioinputplugin.h b/plugins/samplesource/audioinput/audioinputplugin.h new file mode 100644 index 000000000..23976ca78 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputplugin.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOINPUTPLUGIN_H +#define INCLUDE_AUDIOINPUTPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +#define AUDIOINPUT_DEVICE_TYPE_ID "sdrangel.samplesource.audioinput" + +class PluginAPI; + +class AudioInputPlugin : public QObject, public PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID AUDIOINPUT_DEVICE_TYPE_ID) + +public: + explicit AudioInputPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void enumOriginDevices(QStringList& listedHwIds, OriginDevices& originDevices); + virtual SamplingDevices enumSampleSources(const OriginDevices& originDevices); + virtual DeviceGUI* createSampleSourcePluginInstanceGUI( + const QString& sourceId, + QWidget **widget, + DeviceUISet *deviceUISet); + virtual DeviceSampleSource* createSampleSourcePluginInstance(const QString& sourceId, DeviceAPI *deviceAPI); + virtual DeviceWebAPIAdapter* createDeviceWebAPIAdapter() const; + + static const QString m_hardwareID; + static const QString m_deviceTypeID; + +private: + static const PluginDescriptor m_pluginDescriptor; +}; + +#endif // INCLUDE_AUDIOINPUTPLUGIN_H diff --git a/plugins/samplesource/audioinput/audioinputsettings.cpp b/plugins/samplesource/audioinput/audioinputsettings.cpp new file mode 100644 index 000000000..84fb1ae07 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputsettings.cpp @@ -0,0 +1,99 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "util/simpleserializer.h" +#include "audioinputsettings.h" + +AudioInputSettings::AudioInputSettings() +{ + resetToDefaults(); +} + +void AudioInputSettings::resetToDefaults() +{ + m_deviceName = ""; + m_sampleRate = 48000; + m_volume = 1.0f; + m_log2Decim = 0; + m_iqMapping = L; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; +} + +QByteArray AudioInputSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_deviceName); + s.writeS32(2, m_sampleRate); + s.writeFloat(3, m_volume); + s.writeU32(4, m_log2Decim); + s.writeS32(5, (int)m_iqMapping); + + s.writeBool(24, m_useReverseAPI); + s.writeString(25, m_reverseAPIAddress); + s.writeU32(26, m_reverseAPIPort); + s.writeU32(27, m_reverseAPIDeviceIndex); + + return s.final(); +} + +bool AudioInputSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + uint32_t uintval; + + d.readString(1, &m_deviceName, ""); + d.readS32(2, &m_sampleRate, 48000); + d.readFloat(3, &m_volume, 1.0f); + d.readU32(4, &m_log2Decim, 0); + d.readS32(5, (int *)&m_iqMapping, IQMapping::L); + + d.readBool(24, &m_useReverseAPI, false); + d.readString(25, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(26, &uintval, 0); + + if ((uintval > 1023) && (uintval < 65535)) { + m_reverseAPIPort = uintval; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(27, &uintval, 0); + m_reverseAPIDeviceIndex = uintval > 99 ? 99 : uintval; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/samplesource/audioinput/audioinputsettings.h b/plugins/samplesource/audioinput/audioinputsettings.h new file mode 100644 index 000000000..108d965d9 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputsettings.h @@ -0,0 +1,64 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef _AUDIOINPUT_AUDIOINPUTSETTINGS_H_ +#define _AUDIOINPUT_AUDIOINPUTSETTINGS_H_ + +#include +#include + +struct AudioInputSettings { + + QString m_deviceName; // Including realm, as from getFullDeviceName below + int m_sampleRate; + float m_volume; + quint32 m_log2Decim; + enum IQMapping { + L, + R, + LR, + RL + } m_iqMapping; + + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + + AudioInputSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + + // Append realm to device names, because there may be multiple devices with the same name on Windows + static QString getFullDeviceName(const QAudioDeviceInfo &deviceInfo) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + return deviceInfo.deviceName(); +#else + QString realm = deviceInfo.realm(); + if (realm != "" && realm != "default") + return deviceInfo.deviceName() + " " + realm; + else + return deviceInfo.deviceName(); +#endif + } + +}; + +#endif /* _AUDIOINPUT_AUDIOINPUTSETTINGS_H_ */ diff --git a/plugins/samplesource/audioinput/audioinputthread.cpp b/plugins/samplesource/audioinput/audioinputthread.cpp new file mode 100644 index 000000000..9cb3affbd --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputthread.cpp @@ -0,0 +1,141 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // +// written by Christian Daniel // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "audio/audiofifo.h" + +#include "audioinputthread.h" + +AudioInputThread::AudioInputThread(SampleSinkFifo* sampleFifo, AudioFifo *fifo, QObject* parent) : + QThread(parent), + m_fifo(fifo), + m_running(false), + m_log2Decim(0), + m_iqMapping(AudioInputSettings::IQMapping::L), + m_convertBuffer(m_convBufSamples), + m_sampleFifo(sampleFifo) +{ + start(); +} + +AudioInputThread::~AudioInputThread() +{ +} + +void AudioInputThread::startWork() +{ + m_startWaitMutex.lock(); + + start(); + + while(!m_running) + { + m_startWaiter.wait(&m_startWaitMutex, 100); + } + + m_startWaitMutex.unlock(); +} + +void AudioInputThread::stopWork() +{ + m_running = false; + wait(); +} + +void AudioInputThread::run() +{ + m_running = true; + qDebug("AudioInputThread::run: start running loop"); + + while (m_running) + { + workIQ(m_convBufSamples); + } + + qDebug("AudioInputThread::run: running loop stopped"); + m_running = false; +} + +void AudioInputThread::workIQ(unsigned int samples) +{ + // Most of the time, this returns 0, because of the low sample rate. + // Could be more efficient in this case to have blocking wait? + uint32_t nbRead = m_fifo->read((unsigned char *) m_buf, samples); + + // Map between left and right audio channels and IQ channels + if (m_iqMapping == AudioInputSettings::IQMapping::L) + { + for (uint32_t i = 0; i < nbRead; i++) + m_buf[i*2+1] = 0; + } + else if (m_iqMapping == AudioInputSettings::IQMapping::R) + { + for (uint32_t i = 0; i < nbRead; i++) + { + m_buf[i*2] = m_buf[i*2+1]; + m_buf[i*2+1] = 0; + } + } + else if (m_iqMapping == AudioInputSettings::IQMapping::LR) + { + for (uint32_t i = 0; i < nbRead; i++) + { + qint16 t = m_buf[i*2]; + m_buf[i*2] = m_buf[i*2+1]; + m_buf[i*2+1] = t; + } + } + + SampleVector::iterator it = m_convertBuffer.begin(); + + switch (m_log2Decim) + { + case 0: + m_decimatorsIQ.decimate1(&it, m_buf, 2*nbRead); + break; + case 1: + m_decimatorsIQ.decimate2_cen(&it, m_buf, 2*nbRead); + break; + case 2: + m_decimatorsIQ.decimate4_cen(&it, m_buf, 2*nbRead); + break; + case 3: + m_decimatorsIQ.decimate8_cen(&it, m_buf, 2*nbRead); + break; + case 4: + m_decimatorsIQ.decimate16_cen(&it, m_buf, 2*nbRead); + break; + case 5: + m_decimatorsIQ.decimate32_cen(&it, m_buf, 2*nbRead); + break; + case 6: + m_decimatorsIQ.decimate64_cen(&it, m_buf, 2*nbRead); + break; + default: + break; + } + + m_sampleFifo->write(m_convertBuffer.begin(), it); +} diff --git a/plugins/samplesource/audioinput/audioinputthread.h b/plugins/samplesource/audioinput/audioinputthread.h new file mode 100644 index 000000000..3d6a5b3f2 --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputthread.h @@ -0,0 +1,63 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOINPUTTHREAD_H +#define INCLUDE_AUDIOINPUTTHREAD_H + +#include +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/decimators.h" +#include "audioinputsettings.h" + +class AudioFifo; + +class AudioInputThread : public QThread { + Q_OBJECT + +public: + AudioInputThread(SampleSinkFifo* sampleFifo, AudioFifo *fifo, QObject* parent = nullptr); + ~AudioInputThread(); + + void startWork(); + void stopWork(); + void setLog2Decimation(unsigned int log2_decim) {m_log2Decim = log2_decim;} + void setIQMapping(AudioInputSettings::IQMapping iqMapping) {m_iqMapping = iqMapping;} + + static const int m_convBufSamples = 4096; +private: + AudioFifo* m_fifo; + + QMutex m_startWaitMutex; + QWaitCondition m_startWaiter; + bool m_running; + unsigned int m_log2Decim; + AudioInputSettings::IQMapping m_iqMapping; + + qint16 m_buf[m_convBufSamples*2]; // stereo (I, Q) + SampleVector m_convertBuffer; + SampleSinkFifo* m_sampleFifo; + Decimators m_decimatorsIQ; + + void run(); + void workIQ(unsigned int samples); +}; + +#endif // INCLUDE_AUDIOINPUTTHREAD_H diff --git a/plugins/samplesource/audioinput/audioinputwebapiadapter.cpp b/plugins/samplesource/audioinput/audioinputwebapiadapter.cpp new file mode 100644 index 000000000..ae87bbbfc --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputwebapiadapter.cpp @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGDeviceSettings.h" +#include "audioinput.h" +#include "audioinputwebapiadapter.h" + +AudioInputWebAPIAdapter::AudioInputWebAPIAdapter() +{} + +AudioInputWebAPIAdapter::~AudioInputWebAPIAdapter() +{} + +int AudioInputWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAirspyHfSettings(new SWGSDRangel::SWGAirspyHFSettings()); + response.getAirspyHfSettings()->init(); + AudioInputSource::AudioInput::webapiFormatDeviceSettings(response, m_settings); + return 200; +} + +int AudioInputWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage) +{ + (void) errorMessage; + AudioInputSource::AudioInput::webapiUpdateDeviceSettings(m_settings, deviceSettingsKeys, response); + return 200; +} diff --git a/plugins/samplesource/audioinput/audioinputwebapiadapter.h b/plugins/samplesource/audioinput/audioinputwebapiadapter.h new file mode 100644 index 000000000..fc32f9e8a --- /dev/null +++ b/plugins/samplesource/audioinput/audioinputwebapiadapter.h @@ -0,0 +1,42 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "device/devicewebapiadapter.h" +#include "audioinputsettings.h" + +class AudioInputWebAPIAdapter : public DeviceWebAPIAdapter +{ +public: + AudioInputWebAPIAdapter(); + virtual ~AudioInputWebAPIAdapter(); + virtual QByteArray serialize() { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage); + +private: + AudioInputSettings m_settings; +}; diff --git a/plugins/samplesource/audioinput/readme.md b/plugins/samplesource/audioinput/readme.md new file mode 100644 index 000000000..aed7270bb --- /dev/null +++ b/plugins/samplesource/audioinput/readme.md @@ -0,0 +1,46 @@ +

Audio input plugin

+ +

Introduction

+ +This input sample source plugin gets its samples from an audio device. + +

Interface

+ +![Audio input plugin GUI](../../../doc/img/AudioInput_plugin.png) + +

1: Start/Stop

+ +Device start / stop button. + + - Blue triangle icon: device is ready and can be started + - Green square icon: device is running and can be stopped + - Magenta (or pink) square icon: an error occurred. In the case the device was accidentally disconnected you may click on the icon, plug back in and start again. + +

2: Device

+ +The audio device to use. + +

3: Refresh devices

+ +Refresh the list of audio devices. + +

4: Audio sample rate

+ +Audio sample rate in Hz (Sa/s). + +

5: Decimation

+ +A decimation factor to apply to the audio data. The baseband sample rate will be the audio sample, divided by this decimation factor. + +

6: Volume

+ +A control to set the input volume. This is not supported by all input audio devices. + +

7: Channel Map

+ +This controls how the left and right stereo audio channels map on to the IQ channels. + +* I=L, Q=0 - The left audio channel is driven to the I channel. The Q channel is set to 0. +* I=R, Q=0 - The right audio channel is driven to the I channel. The Q channel is set to 0. +* I=L, Q=R - The left audio channel is driven to the I channel. The right audio channel is driven to the Q channel. +* I=R, Q=L - The right audio channel is driven to the I channel. The left audio channel is driven to the Q channel. diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 3fb307511..6d52edf4f 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -1574,6 +1574,44 @@ margin-bottom: 20px; } }, "description" : "Audio input device" +}; + defs.AudioInputSettings = { + "properties" : { + "device" : { + "type" : "string", + "description" : "The name of the audio device" + }, + "devSampleRate" : { + "type" : "integer", + "description" : "Audio sample rate" + }, + "volume" : { + "type" : "number", + "format" : "float" + }, + "log2Decim" : { + "type" : "integer", + "description" : "Decimation factor" + }, + "iqMapping" : { + "type" : "integer", + "description" : "Audio channel to IQ mapping\n * 0 - I=L, Q=0\n * 1 - I=R, Q=0\n * 2 - I=L, Q=R\n * 3 - I=R, Q=L\n" + }, + "useReverseAPI" : { + "type" : "integer", + "description" : "Synchronize with reverse API (1 for yes, 0 for no)" + }, + "reverseAPIAddress" : { + "type" : "string" + }, + "reverseAPIPort" : { + "type" : "integer" + }, + "reverseAPIDeviceIndex" : { + "type" : "integer" + } + }, + "description" : "AudioInput" }; defs.AudioOutputDevice = { "properties" : { @@ -3058,6 +3096,9 @@ margin-bottom: 20px; "airspyHFSettings" : { "$ref" : "#/definitions/AirspyHFSettings" }, + "audioInputSettings" : { + "$ref" : "#/definitions/AudioInputSettings" + }, "bladeRF1InputSettings" : { "$ref" : "#/definitions/BladeRF1InputSettings" }, @@ -40214,7 +40255,7 @@ except ApiException as e:
- Generated 2020-10-27T23:03:34.026+01:00 + Generated 2020-11-09T16:43:08.421+01:00
diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index a204e23cf..63b68752c 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -3789,6 +3789,11 @@ bool WebAPIRequestMapper::getDeviceSettings( deviceSettings->setAirspyHfSettings(new SWGSDRangel::SWGAirspyHFSettings()); deviceSettings->getAirspyHfSettings()->fromJsonObject(settingsJsonObject); } + else if (deviceSettingsKey == "audioInputSettings") + { + deviceSettings->setAudioInputSettings(new SWGSDRangel::SWGAudioInputSettings()); + deviceSettings->getAudioInputSettings()->fromJsonObject(settingsJsonObject); + } else if (deviceSettingsKey == "bladeRF1InputSettings") { deviceSettings->setBladeRf1InputSettings(new SWGSDRangel::SWGBladeRF1InputSettings()); @@ -4098,6 +4103,7 @@ void WebAPIRequestMapper::resetDeviceSettings(SWGSDRangel::SWGDeviceSettings& de deviceSettings.setDeviceHwType(nullptr); deviceSettings.setAirspySettings(nullptr); deviceSettings.setAirspyHfSettings(nullptr); + deviceSettings.setAudioInputSettings(nullptr); deviceSettings.setBladeRf1InputSettings(nullptr); deviceSettings.setBladeRf1OutputSettings(nullptr); deviceSettings.setFcdProPlusSettings(nullptr); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index f392c639f..716555541 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -68,6 +68,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { const QMap WebAPIUtils::m_deviceIdToSettingsKey = { {"sdrangel.samplesource.airspy", "airspySettings"}, {"sdrangel.samplesource.airspyhf", "airspyHFSettings"}, + {"sdrangel.samplesource.audio", "audioInputSettings"}, {"sdrangel.samplesource.bladerf1input", "bladeRF1InputSettings"}, {"sdrangel.samplesource.bladerf", "bladeRF1InputSettings"}, // remap {"sdrangel.samplesink.bladerf1output", "bladeRF1OutputSettings"}, @@ -156,6 +157,7 @@ const QMap WebAPIUtils::m_channelTypeToActionsKey = { const QMap WebAPIUtils::m_sourceDeviceHwIdToSettingsKey = { {"Airspy", "airspySettings"}, {"AirspyHF", "airspyHFSettings"}, + {"AudioInput", "audioInputSettings"}, {"BladeRF1", "bladeRF1InputSettings"}, {"BladeRF2", "bladeRF2InputSettings"}, {"FCDPro", "fcdProSettings"}, @@ -180,6 +182,7 @@ const QMap WebAPIUtils::m_sourceDeviceHwIdToSettingsKey = { const QMap WebAPIUtils::m_sourceDeviceHwIdToActionsKey = { {"Airspy", "airspyActions"}, {"AirspyHF", "airspyHFActions"}, + {"AudioInput", "audioInputActions"}, {"BladeRF1", "bladeRF1InputActions"}, {"FCDPro", "fcdProActions"}, {"FCDPro+", "fcdProPlusActions"}, diff --git a/swagger/sdrangel/api/swagger/include/AudioInput.yaml b/swagger/sdrangel/api/swagger/include/AudioInput.yaml new file mode 100644 index 000000000..540442dd8 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/AudioInput.yaml @@ -0,0 +1,32 @@ +AudioInputSettings: + description: AudioInput + properties: + device: + description: The name of the audio device + type: string + devSampleRate: + description: Audio sample rate + type: integer + volume: + type: number + format: float + log2Decim: + description: Decimation factor + type: integer + iqMapping: + type: integer + description: > + Audio channel to IQ mapping + * 0 - I=L, Q=0 + * 1 - I=R, Q=0 + * 2 - I=L, Q=R + * 3 - I=R, Q=L + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer diff --git a/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml b/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml index dffaedf40..1264e24bb 100644 --- a/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml @@ -18,6 +18,8 @@ DeviceSettings: $ref: "http://swgserver:8081/api/swagger/include/Airspy.yaml#/AirspySettings" airspyHFSettings: $ref: "http://swgserver:8081/api/swagger/include/AirspyHF.yaml#/AirspyHFSettings" + audioInputSettings: + $ref: "http://swgserver:8081/api/swagger/include/AudioInput.yaml#/AudioInputSettings" bladeRF1InputSettings: $ref: "http://swgserver:8081/api/swagger/include/BladeRF1.yaml#/BladeRF1InputSettings" bladeRF2InputSettings: diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index 3fb307511..6d52edf4f 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -1574,6 +1574,44 @@ margin-bottom: 20px; } }, "description" : "Audio input device" +}; + defs.AudioInputSettings = { + "properties" : { + "device" : { + "type" : "string", + "description" : "The name of the audio device" + }, + "devSampleRate" : { + "type" : "integer", + "description" : "Audio sample rate" + }, + "volume" : { + "type" : "number", + "format" : "float" + }, + "log2Decim" : { + "type" : "integer", + "description" : "Decimation factor" + }, + "iqMapping" : { + "type" : "integer", + "description" : "Audio channel to IQ mapping\n * 0 - I=L, Q=0\n * 1 - I=R, Q=0\n * 2 - I=L, Q=R\n * 3 - I=R, Q=L\n" + }, + "useReverseAPI" : { + "type" : "integer", + "description" : "Synchronize with reverse API (1 for yes, 0 for no)" + }, + "reverseAPIAddress" : { + "type" : "string" + }, + "reverseAPIPort" : { + "type" : "integer" + }, + "reverseAPIDeviceIndex" : { + "type" : "integer" + } + }, + "description" : "AudioInput" }; defs.AudioOutputDevice = { "properties" : { @@ -3058,6 +3096,9 @@ margin-bottom: 20px; "airspyHFSettings" : { "$ref" : "#/definitions/AirspyHFSettings" }, + "audioInputSettings" : { + "$ref" : "#/definitions/AudioInputSettings" + }, "bladeRF1InputSettings" : { "$ref" : "#/definitions/BladeRF1InputSettings" }, @@ -40214,7 +40255,7 @@ except ApiException as e:
- Generated 2020-10-27T23:03:34.026+01:00 + Generated 2020-11-09T16:43:08.421+01:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.cpp new file mode 100644 index 000000000..ea3421e5d --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.cpp @@ -0,0 +1,296 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1, USRP and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 4.15.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAudioInputSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAudioInputSettings::SWGAudioInputSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAudioInputSettings::SWGAudioInputSettings() { + device = nullptr; + m_device_isSet = false; + dev_sample_rate = 0; + m_dev_sample_rate_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + log2_decim = 0; + m_log2_decim_isSet = false; + iq_mapping = 0; + m_iq_mapping_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; +} + +SWGAudioInputSettings::~SWGAudioInputSettings() { + this->cleanup(); +} + +void +SWGAudioInputSettings::init() { + device = new QString(""); + m_device_isSet = false; + dev_sample_rate = 0; + m_dev_sample_rate_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + log2_decim = 0; + m_log2_decim_isSet = false; + iq_mapping = 0; + m_iq_mapping_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; +} + +void +SWGAudioInputSettings::cleanup() { + if(device != nullptr) { + delete device; + } + + + + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + +} + +SWGAudioInputSettings* +SWGAudioInputSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAudioInputSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&device, pJson["device"], "QString", "QString"); + + ::SWGSDRangel::setValue(&dev_sample_rate, pJson["devSampleRate"], "qint32", ""); + + ::SWGSDRangel::setValue(&volume, pJson["volume"], "float", ""); + + ::SWGSDRangel::setValue(&log2_decim, pJson["log2Decim"], "qint32", ""); + + ::SWGSDRangel::setValue(&iq_mapping, pJson["iqMapping"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + +} + +QString +SWGAudioInputSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAudioInputSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(device != nullptr && *device != QString("")){ + toJsonValue(QString("device"), device, obj, QString("QString")); + } + if(m_dev_sample_rate_isSet){ + obj->insert("devSampleRate", QJsonValue(dev_sample_rate)); + } + if(m_volume_isSet){ + obj->insert("volume", QJsonValue(volume)); + } + if(m_log2_decim_isSet){ + obj->insert("log2Decim", QJsonValue(log2_decim)); + } + if(m_iq_mapping_isSet){ + obj->insert("iqMapping", QJsonValue(iq_mapping)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + + return obj; +} + +QString* +SWGAudioInputSettings::getDevice() { + return device; +} +void +SWGAudioInputSettings::setDevice(QString* device) { + this->device = device; + this->m_device_isSet = true; +} + +qint32 +SWGAudioInputSettings::getDevSampleRate() { + return dev_sample_rate; +} +void +SWGAudioInputSettings::setDevSampleRate(qint32 dev_sample_rate) { + this->dev_sample_rate = dev_sample_rate; + this->m_dev_sample_rate_isSet = true; +} + +float +SWGAudioInputSettings::getVolume() { + return volume; +} +void +SWGAudioInputSettings::setVolume(float volume) { + this->volume = volume; + this->m_volume_isSet = true; +} + +qint32 +SWGAudioInputSettings::getLog2Decim() { + return log2_decim; +} +void +SWGAudioInputSettings::setLog2Decim(qint32 log2_decim) { + this->log2_decim = log2_decim; + this->m_log2_decim_isSet = true; +} + +qint32 +SWGAudioInputSettings::getIqMapping() { + return iq_mapping; +} +void +SWGAudioInputSettings::setIqMapping(qint32 iq_mapping) { + this->iq_mapping = iq_mapping; + this->m_iq_mapping_isSet = true; +} + +qint32 +SWGAudioInputSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGAudioInputSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGAudioInputSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGAudioInputSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGAudioInputSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGAudioInputSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGAudioInputSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGAudioInputSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + + +bool +SWGAudioInputSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(device && *device != QString("")){ + isObjectUpdated = true; break; + } + if(m_dev_sample_rate_isSet){ + isObjectUpdated = true; break; + } + if(m_volume_isSet){ + isObjectUpdated = true; break; + } + if(m_log2_decim_isSet){ + isObjectUpdated = true; break; + } + if(m_iq_mapping_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.h b/swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.h new file mode 100644 index 000000000..c227d338d --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAudioInputSettings.h @@ -0,0 +1,107 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1, USRP and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 4.15.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAudioInputSettings.h + * + * AudioInput + */ + +#ifndef SWGAudioInputSettings_H_ +#define SWGAudioInputSettings_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAudioInputSettings: public SWGObject { +public: + SWGAudioInputSettings(); + SWGAudioInputSettings(QString* json); + virtual ~SWGAudioInputSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAudioInputSettings* fromJson(QString &jsonString) override; + + QString* getDevice(); + void setDevice(QString* device); + + qint32 getDevSampleRate(); + void setDevSampleRate(qint32 dev_sample_rate); + + float getVolume(); + void setVolume(float volume); + + qint32 getLog2Decim(); + void setLog2Decim(qint32 log2_decim); + + qint32 getIqMapping(); + void setIqMapping(qint32 iq_mapping); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + + virtual bool isSet() override; + +private: + QString* device; + bool m_device_isSet; + + qint32 dev_sample_rate; + bool m_dev_sample_rate_isSet; + + float volume; + bool m_volume_isSet; + + qint32 log2_decim; + bool m_log2_decim_isSet; + + qint32 iq_mapping; + bool m_iq_mapping_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + +}; + +} + +#endif /* SWGAudioInputSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp index f7523e093..9b4d9c73a 100644 --- a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp @@ -38,6 +38,8 @@ SWGDeviceSettings::SWGDeviceSettings() { m_airspy_settings_isSet = false; airspy_hf_settings = nullptr; m_airspy_hf_settings_isSet = false; + audio_input_settings = nullptr; + m_audio_input_settings_isSet = false; blade_rf1_input_settings = nullptr; m_blade_rf1_input_settings_isSet = false; blade_rf2_input_settings = nullptr; @@ -114,6 +116,8 @@ SWGDeviceSettings::init() { m_airspy_settings_isSet = false; airspy_hf_settings = new SWGAirspyHFSettings(); m_airspy_hf_settings_isSet = false; + audio_input_settings = new SWGAudioInputSettings(); + m_audio_input_settings_isSet = false; blade_rf1_input_settings = new SWGBladeRF1InputSettings(); m_blade_rf1_input_settings_isSet = false; blade_rf2_input_settings = new SWGBladeRF2InputSettings(); @@ -187,6 +191,9 @@ SWGDeviceSettings::cleanup() { if(airspy_hf_settings != nullptr) { delete airspy_hf_settings; } + if(audio_input_settings != nullptr) { + delete audio_input_settings; + } if(blade_rf1_input_settings != nullptr) { delete blade_rf1_input_settings; } @@ -297,6 +304,8 @@ SWGDeviceSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&airspy_hf_settings, pJson["airspyHFSettings"], "SWGAirspyHFSettings", "SWGAirspyHFSettings"); + ::SWGSDRangel::setValue(&audio_input_settings, pJson["audioInputSettings"], "SWGAudioInputSettings", "SWGAudioInputSettings"); + ::SWGSDRangel::setValue(&blade_rf1_input_settings, pJson["bladeRF1InputSettings"], "SWGBladeRF1InputSettings", "SWGBladeRF1InputSettings"); ::SWGSDRangel::setValue(&blade_rf2_input_settings, pJson["bladeRF2InputSettings"], "SWGBladeRF2InputSettings", "SWGBladeRF2InputSettings"); @@ -386,6 +395,9 @@ SWGDeviceSettings::asJsonObject() { if((airspy_hf_settings != nullptr) && (airspy_hf_settings->isSet())){ toJsonValue(QString("airspyHFSettings"), airspy_hf_settings, obj, QString("SWGAirspyHFSettings")); } + if((audio_input_settings != nullptr) && (audio_input_settings->isSet())){ + toJsonValue(QString("audioInputSettings"), audio_input_settings, obj, QString("SWGAudioInputSettings")); + } if((blade_rf1_input_settings != nullptr) && (blade_rf1_input_settings->isSet())){ toJsonValue(QString("bladeRF1InputSettings"), blade_rf1_input_settings, obj, QString("SWGBladeRF1InputSettings")); } @@ -527,6 +539,16 @@ SWGDeviceSettings::setAirspyHfSettings(SWGAirspyHFSettings* airspy_hf_settings) this->m_airspy_hf_settings_isSet = true; } +SWGAudioInputSettings* +SWGDeviceSettings::getAudioInputSettings() { + return audio_input_settings; +} +void +SWGDeviceSettings::setAudioInputSettings(SWGAudioInputSettings* audio_input_settings) { + this->audio_input_settings = audio_input_settings; + this->m_audio_input_settings_isSet = true; +} + SWGBladeRF1InputSettings* SWGDeviceSettings::getBladeRf1InputSettings() { return blade_rf1_input_settings; @@ -837,6 +859,9 @@ SWGDeviceSettings::isSet(){ if(airspy_hf_settings && airspy_hf_settings->isSet()){ isObjectUpdated = true; break; } + if(audio_input_settings && audio_input_settings->isSet()){ + isObjectUpdated = true; break; + } if(blade_rf1_input_settings && blade_rf1_input_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h index 29b912654..d97af0475 100644 --- a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h @@ -24,6 +24,7 @@ #include "SWGAirspyHFSettings.h" #include "SWGAirspySettings.h" +#include "SWGAudioInputSettings.h" #include "SWGBladeRF1InputSettings.h" #include "SWGBladeRF1OutputSettings.h" #include "SWGBladeRF2InputSettings.h" @@ -88,6 +89,9 @@ public: SWGAirspyHFSettings* getAirspyHfSettings(); void setAirspyHfSettings(SWGAirspyHFSettings* airspy_hf_settings); + SWGAudioInputSettings* getAudioInputSettings(); + void setAudioInputSettings(SWGAudioInputSettings* audio_input_settings); + SWGBladeRF1InputSettings* getBladeRf1InputSettings(); void setBladeRf1InputSettings(SWGBladeRF1InputSettings* blade_rf1_input_settings); @@ -194,6 +198,9 @@ private: SWGAirspyHFSettings* airspy_hf_settings; bool m_airspy_hf_settings_isSet; + SWGAudioInputSettings* audio_input_settings; + bool m_audio_input_settings_isSet; + SWGBladeRF1InputSettings* blade_rf1_input_settings; bool m_blade_rf1_input_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index f54e54fb3..1d8256076 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -36,6 +36,7 @@ #include "SWGArgValue.h" #include "SWGAudioDevices.h" #include "SWGAudioInputDevice.h" +#include "SWGAudioInputSettings.h" #include "SWGAudioOutputDevice.h" #include "SWGBFMDemodReport.h" #include "SWGBFMDemodSettings.h" @@ -275,6 +276,9 @@ namespace SWGSDRangel { if(QString("SWGAudioInputDevice").compare(type) == 0) { return new SWGAudioInputDevice(); } + if(QString("SWGAudioInputSettings").compare(type) == 0) { + return new SWGAudioInputSettings(); + } if(QString("SWGAudioOutputDevice").compare(type) == 0) { return new SWGAudioOutputDevice(); } From 4fd64d11b3fb307e7a53eded70a70352d5c719bc Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 9 Nov 2020 16:04:13 +0000 Subject: [PATCH 3/6] Fix readme markup --- plugins/samplesource/audioinput/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/samplesource/audioinput/readme.md b/plugins/samplesource/audioinput/readme.md index aed7270bb..c60ba3831 100644 --- a/plugins/samplesource/audioinput/readme.md +++ b/plugins/samplesource/audioinput/readme.md @@ -20,7 +20,7 @@ Device start / stop button. The audio device to use. -

3: Refresh devices

+

3: Refresh devices

Refresh the list of audio devices. From 330507773c7c0dba85a03b2ae48fedc408e85e89 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 9 Nov 2020 16:14:20 +0000 Subject: [PATCH 4/6] Set reverse API keys. Avoid restarting thread --- plugins/samplesource/audioinput/audioinput.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/samplesource/audioinput/audioinput.cpp b/plugins/samplesource/audioinput/audioinput.cpp index e03173627..2e0987d98 100644 --- a/plugins/samplesource/audioinput/audioinput.cpp +++ b/plugins/samplesource/audioinput/audioinput.cpp @@ -116,13 +116,14 @@ bool AudioInput::start() return false; } + // Call before creating thread, so we don't immediately stop it + applySettings(m_settings, true); + m_thread = new AudioInputThread(&m_sampleFifo, &m_fifo); m_thread->setLog2Decimation(m_settings.m_log2Decim); m_thread->setIQMapping(m_settings.m_iqMapping); m_thread->startWork(); - applySettings(m_settings, true); - qDebug("AudioInput::started"); m_running = true; @@ -249,18 +250,18 @@ void AudioInput::applySettings(const AudioInputSettings& settings, bool force) if ((m_settings.m_deviceName != settings.m_deviceName) || force) { - //reverseAPIKeys.append("device"); + reverseAPIKeys.append("device"); } if ((m_settings.m_sampleRate != settings.m_sampleRate) || force) { - //reverseAPIKeys.append("sampleRate"); + reverseAPIKeys.append("sampleRate"); forwardChange = true; } if ((m_settings.m_volume != settings.m_volume) || force) { - //reverseAPIKeys.append("volume"); + reverseAPIKeys.append("volume"); m_audioInput.setVolume(settings.m_volume); qDebug() << "AudioInput::applySettings: set volume to " << settings.m_volume; } @@ -279,7 +280,7 @@ void AudioInput::applySettings(const AudioInputSettings& settings, bool force) if ((m_settings.m_iqMapping != settings.m_iqMapping) || force) { - //reverseAPIKeys.append("iqMapping"); + reverseAPIKeys.append("iqMapping"); if (m_thread) m_thread->setIQMapping(settings.m_iqMapping); } From 90f450b25cb95b5ea5e6aa8dce816a2c952dcd93 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 9 Nov 2020 20:55:23 +0000 Subject: [PATCH 5/6] Don't add alsa realm suffix, as there do not appear to be duplicate names --- plugins/samplesource/audioinput/audioinputsettings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/samplesource/audioinput/audioinputsettings.h b/plugins/samplesource/audioinput/audioinputsettings.h index 108d965d9..16f8eb4f1 100644 --- a/plugins/samplesource/audioinput/audioinputsettings.h +++ b/plugins/samplesource/audioinput/audioinputsettings.h @@ -52,7 +52,7 @@ struct AudioInputSettings { return deviceInfo.deviceName(); #else QString realm = deviceInfo.realm(); - if (realm != "" && realm != "default") + if (realm != "" && realm != "default" && realm != "alsa") return deviceInfo.deviceName() + " " + realm; else return deviceInfo.deviceName(); From 181d8cfcfe68df59c154bb428dbb22f13efdf4a2 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Mon, 9 Nov 2020 20:56:15 +0000 Subject: [PATCH 6/6] Don't call openAudioDevice if called from start(), otherwise AudioInput will be created on wrong thread --- .../samplesource/audioinput/audioinput.cpp | 23 +++++++++++-------- plugins/samplesource/audioinput/audioinput.h | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/plugins/samplesource/audioinput/audioinput.cpp b/plugins/samplesource/audioinput/audioinput.cpp index 2e0987d98..80e136090 100644 --- a/plugins/samplesource/audioinput/audioinput.cpp +++ b/plugins/samplesource/audioinput/audioinput.cpp @@ -89,7 +89,7 @@ bool AudioInput::openAudioDevice(QString deviceName, qint32 sampleRate) { if (AudioInputSettings::getFullDeviceName(itAudio) == deviceName) { - // FIXME: getInputDeviceIndex needs a realm parameter (itAudio.realm()) - need to look on Linux to see if it uses default? + // FIXME: getInputDeviceIndex needs a realm parameter (itAudio.realm()) int deviceIndex = audioDeviceManager->getInputDeviceIndex(itAudio.deviceName()); m_audioInput.start(deviceIndex, sampleRate); m_audioInput.addFifo(&m_fifo); @@ -116,14 +116,14 @@ bool AudioInput::start() return false; } - // Call before creating thread, so we don't immediately stop it - applySettings(m_settings, true); + applySettings(m_settings, true, true); m_thread = new AudioInputThread(&m_sampleFifo, &m_fifo); m_thread->setLog2Decimation(m_settings.m_log2Decim); m_thread->setIQMapping(m_settings.m_iqMapping); m_thread->startWork(); + qDebug("AudioInput::started"); m_running = true; @@ -233,7 +233,7 @@ bool AudioInput::handleMessage(const Message& message) } } -void AudioInput::applySettings(const AudioInputSettings& settings, bool force) +void AudioInput::applySettings(const AudioInputSettings& settings, bool force, bool starting) { bool forwardChange = false; QList reverseAPIKeys; @@ -241,11 +241,16 @@ void AudioInput::applySettings(const AudioInputSettings& settings, bool force) if ((m_settings.m_deviceName != settings.m_deviceName) || (m_settings.m_sampleRate != settings.m_sampleRate) || force) { - closeDevice(); - if (openAudioDevice(settings.m_deviceName, settings.m_sampleRate)) - qDebug() << "AudioInput::applySettings: opened device " << settings.m_deviceName << " with sample rate " << m_audioInput.getRate(); - else - qCritical() << "AudioInput::applySettings: failed to open device " << settings.m_deviceName; + // Don't call openAudioDevice if called from start(), otherwise ::AudioInput + // will be created on wrong thread and we'll crash after ::AudioInput::stop calls delete + if (!starting) + { + closeDevice(); + if (openAudioDevice(settings.m_deviceName, settings.m_sampleRate)) + qDebug() << "AudioInput::applySettings: opened device " << settings.m_deviceName << " with sample rate " << m_audioInput.getRate(); + else + qCritical() << "AudioInput::applySettings: failed to open device " << settings.m_deviceName; + } } if ((m_settings.m_deviceName != settings.m_deviceName) || force) diff --git a/plugins/samplesource/audioinput/audioinput.h b/plugins/samplesource/audioinput/audioinput.h index 2632acbfa..5849f260c 100644 --- a/plugins/samplesource/audioinput/audioinput.h +++ b/plugins/samplesource/audioinput/audioinput.h @@ -147,7 +147,7 @@ private: bool openDevice(); void closeDevice(); bool openAudioDevice(QString deviceName, int sampleRate); - void applySettings(const AudioInputSettings& settings, bool force); + void applySettings(const AudioInputSettings& settings, bool force, bool starting=false); void webapiReverseSendSettings(QList& deviceSettingsKeys, const AudioInputSettings& settings, bool force); void webapiReverseSendStartStop(bool start);