From aac8f6fe2cda181f29184951fb5b567d84720e8d Mon Sep 17 00:00:00 2001 From: f4exb Date: Thu, 22 Apr 2021 22:10:04 +0200 Subject: [PATCH] APT demod: moved processPixels process to a separate thread --- plugins/channelrx/demodapt/CMakeLists.txt | 2 + plugins/channelrx/demodapt/aptdemod.cpp | 58 +++- plugins/channelrx/demodapt/aptdemod.h | 14 + plugins/channelrx/demodapt/aptdemodbaseband.h | 2 +- .../demodapt/aptdemodimageworker.cpp | 279 ++++++++++++++++++ .../channelrx/demodapt/aptdemodimageworker.h | 100 +++++++ plugins/channelrx/demodapt/aptdemodsink.cpp | 8 +- plugins/channelrx/demodapt/aptdemodsink.h | 6 +- 8 files changed, 456 insertions(+), 13 deletions(-) create mode 100644 plugins/channelrx/demodapt/aptdemodimageworker.cpp create mode 100644 plugins/channelrx/demodapt/aptdemodimageworker.h diff --git a/plugins/channelrx/demodapt/CMakeLists.txt b/plugins/channelrx/demodapt/CMakeLists.txt index 2eeacf6de..a097d181b 100644 --- a/plugins/channelrx/demodapt/CMakeLists.txt +++ b/plugins/channelrx/demodapt/CMakeLists.txt @@ -5,6 +5,7 @@ set(demodapt_SOURCES aptdemodsettings.cpp aptdemodbaseband.cpp aptdemodsink.cpp + aptdemodimageworker.cpp aptdemodplugin.cpp aptdemodwebapiadapter.cpp ) @@ -14,6 +15,7 @@ set(demodapt_HEADERS aptdemodsettings.h aptdemodbaseband.h aptdemodsink.h + aptdemodimageworker.h aptdemodplugin.h aptdemodwebapiadapter.h ) diff --git a/plugins/channelrx/demodapt/aptdemod.cpp b/plugins/channelrx/demodapt/aptdemod.cpp index 82cb70390..28c16dca4 100644 --- a/plugins/channelrx/demodapt/aptdemod.cpp +++ b/plugins/channelrx/demodapt/aptdemod.cpp @@ -58,9 +58,12 @@ APTDemod::APTDemod(DeviceAPI *deviceAPI) : setObjectName(m_channelId); m_basebandSink = new APTDemodBaseband(this); - m_basebandSink->setMessageQueueToChannel(getInputMessageQueue()); m_basebandSink->moveToThread(&m_thread); + m_imageWorker = new APTDemodImageWorker(); + m_basebandSink->setImagWorkerMessageQueue(m_imageWorker->getInputMessageQueue()); + m_imageWorker->moveToThread(&m_imageThread); + applySettings(m_settings, true); m_deviceAPI->addChannelSink(this); @@ -74,7 +77,8 @@ APTDemod::APTDemod(DeviceAPI *deviceAPI) : m_image.prow[y] = new float[APT_PROW_WIDTH]; m_tempImage.prow[y] = new float[APT_PROW_WIDTH]; } - resetDecoder(); + + resetDecoder(); // FIXME: to be removed } APTDemod::~APTDemod() @@ -85,16 +89,22 @@ APTDemod::~APTDemod() m_deviceAPI->removeChannelSinkAPI(this); m_deviceAPI->removeChannelSink(this); + if (m_imageWorker->isRunning()) { + stopImageWorker(); + } + + delete m_imageWorker; + if (m_basebandSink->isRunning()) { - stop(); + stopBasebandSink(); } delete m_basebandSink; for (int y = 0; y < APT_MAX_HEIGHT; y++) { - delete m_image.prow[y]; - delete m_tempImage.prow[y]; + delete[] m_image.prow[y]; + delete[] m_tempImage.prow[y]; } } @@ -110,6 +120,12 @@ void APTDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto } void APTDemod::start() +{ + startBasebandSink(); + startImageWorker(); +} + +void APTDemod::startBasebandSink() { qDebug("APTDemod::start"); @@ -124,7 +140,25 @@ void APTDemod::start() m_basebandSink->getInputMessageQueue()->push(msg); } +void APTDemod::startImageWorker() +{ + qDebug("APTDemod::startImageWorker"); + + m_imageWorker->reset(); + m_imageWorker->startWork(); + m_imageThread.start(); + + APTDemodImageWorker::MsgConfigureAPTDemodImageWorker *msg = APTDemodImageWorker::MsgConfigureAPTDemodImageWorker::create(m_settings, true); + m_imageWorker->getInputMessageQueue()->push(msg); +} + void APTDemod::stop() +{ + stopImageWorker(); + stopBasebandSink(); +} + +void APTDemod::stopBasebandSink() { qDebug("APTDemod::stop"); m_basebandSink->stopWork(); @@ -132,6 +166,14 @@ void APTDemod::stop() m_thread.wait(); } +void APTDemod::stopImageWorker() +{ + qDebug("APTDemod::stopImageWorker"); + m_imageWorker->stopWork(); + m_imageThread.quit(); + m_imageThread.wait(); +} + bool APTDemod::matchSatellite(const QString satelliteName) { return m_settings.m_satelliteTrackerControl @@ -176,7 +218,8 @@ bool APTDemod::handleMessage(const Message& cmd) } else if (APTDemod::MsgResetDecoder::match(cmd)) { - resetDecoder(); + resetDecoder(); // FIXME: to be removed + m_imageWorker->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); // Forward to sink m_basebandSink->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); return true; @@ -579,7 +622,8 @@ int APTDemod::webapiActionsPost( if (matchSatellite(*satelliteName)) { // Reset for new pass - resetDecoder(); + resetDecoder(); // FIXME: to be removed + m_imageWorker->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); m_basebandSink->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); // Save satellite name diff --git a/plugins/channelrx/demodapt/aptdemod.h b/plugins/channelrx/demodapt/aptdemod.h index f38341e6a..e2982b189 100644 --- a/plugins/channelrx/demodapt/aptdemod.h +++ b/plugins/channelrx/demodapt/aptdemod.h @@ -32,12 +32,14 @@ #include "util/message.h" #include "aptdemodbaseband.h" +#include "aptdemodimageworker.h" #include "aptdemodsettings.h" class QNetworkAccessManager; class QNetworkReply; class QThread; class DeviceAPI; +class APTDemodImageWorker; class APTDemod : public BasebandSampleSink, public ChannelAPI { Q_OBJECT @@ -144,8 +146,18 @@ public: virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); virtual void start(); virtual void stop(); + virtual void startBasebandSink(); + virtual void stopBasebandSink(); + virtual void startImageWorker(); + virtual void stopImageWorker(); virtual bool handleMessage(const Message& cmd); + void setMessageQueueToGUI(MessageQueue* queue) override + { + ChannelAPI::setMessageQueueToGUI(queue); + m_imageWorker->setMessageQueueToGUI(queue); + } + virtual void getIdentifier(QString& id) { id = objectName(); } virtual const QString& getURI() const { return getName(); } virtual void getTitle(QString& title) { title = m_settings.m_title; } @@ -202,7 +214,9 @@ public: private: DeviceAPI *m_deviceAPI; QThread m_thread; + QThread m_imageThread; APTDemodBaseband* m_basebandSink; + APTDemodImageWorker *m_imageWorker; APTDemodSettings m_settings; int m_basebandSampleRate; //!< stored from device message used when starting baseband sink qint64 m_centerFrequency; diff --git a/plugins/channelrx/demodapt/aptdemodbaseband.h b/plugins/channelrx/demodapt/aptdemodbaseband.h index 8acbf26fe..2d5a0e5a2 100644 --- a/plugins/channelrx/demodapt/aptdemodbaseband.h +++ b/plugins/channelrx/demodapt/aptdemodbaseband.h @@ -68,7 +68,7 @@ public: void getMagSqLevels(double& avg, double& peak, int& nbSamples) { m_sink.getMagSqLevels(avg, peak, nbSamples); } - void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } + void setImagWorkerMessageQueue(MessageQueue *messageQueue) { m_sink.setImageWorkerMessageQueue(messageQueue); } void setBasebandSampleRate(int sampleRate); double getMagSq() const { return m_sink.getMagSq(); } bool isRunning() const { return m_running; } diff --git a/plugins/channelrx/demodapt/aptdemodimageworker.cpp b/plugins/channelrx/demodapt/aptdemodimageworker.cpp new file mode 100644 index 000000000..e75214077 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodimageworker.cpp @@ -0,0 +1,279 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 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 "aptdemod.h" +#include "aptdemodimageworker.h" + +MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgConfigureAPTDemodImageWorker, Message) + +APTDemodImageWorker::APTDemodImageWorker() : + m_messageQueueToGUI(nullptr), + m_running(false), + m_mutex(QMutex::Recursive) +{ + for (int y = 0; y < APT_MAX_HEIGHT; y++) + { + m_image.prow[y] = new float[APT_PROW_WIDTH]; + m_tempImage.prow[y] = new float[APT_PROW_WIDTH]; + } + + resetDecoder(); +} + +APTDemodImageWorker::~APTDemodImageWorker() +{ + m_inputMessageQueue.clear(); + + for (int y = 0; y < APT_MAX_HEIGHT; y++) + { + delete[] m_image.prow[y]; + delete[] m_tempImage.prow[y]; + } +} + +void APTDemodImageWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +void APTDemodImageWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void APTDemodImageWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = false; +} + +void APTDemodImageWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool APTDemodImageWorker::handleMessage(const Message& cmd) +{ + if (MsgConfigureAPTDemodImageWorker::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureAPTDemodImageWorker& cfg = (MsgConfigureAPTDemodImageWorker&) cmd; + qDebug("APTDemodImageWorker::handleMessage: MsgConfigureAPTDemodImageWorker"); + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else if (APTDemod::MsgPixels::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + const APTDemod::MsgPixels& pixelsMsg = (APTDemod::MsgPixels&) cmd; + const float *pixels = pixelsMsg.getPixels(); + processPixels(pixels); + return true; + } + else if (APTDemod::MsgResetDecoder::match(cmd)) + { + resetDecoder(); + return true; + } + else + { + return false; + } +} + +void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool force) +{ + (void) force; + m_settings = settings; +} + +void APTDemodImageWorker::resetDecoder() +{ + m_image.nrow = 0; + m_tempImage.nrow = 0; + m_greyImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_Grayscale8); + m_greyImage.fill(0); + m_colourImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_RGB888); + m_colourImage.fill(0); + m_satelliteName = ""; +} + +void APTDemodImageWorker::processPixels(const float *pixels) +{ + std::copy(pixels, pixels + APT_PROW_WIDTH, m_image.prow[m_image.nrow]); + m_image.nrow++; + sendImageToGUI(); +} + +void APTDemodImageWorker::sendImageToGUI() +{ + // Send image to GUI + if (m_messageQueueToGUI) + { + QStringList imageTypes; + QImage image = processImage(imageTypes); + m_messageQueueToGUI->push(APTDemod::MsgImage::create(image, imageTypes, m_satelliteName)); + } +} + +QImage APTDemodImageWorker::processImage(QStringList& imageTypes) +{ + copyImage(&m_tempImage, &m_image); + + // Calibrate channels according to wavelength + if (m_tempImage.nrow >= APT_CALIBRATION_ROWS) + { + m_tempImage.chA = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + m_tempImage.chB = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + QStringList channelTypes({ + "", // Unknown + "Visible (0.58-0.68 um)", + "Near-IR (0.725-1.0 um)", + "Near-IR (1.58-1.64 um)", + "Mid-infrared (3.55-3.93 um)", + "Thermal-infrared (10.3-11.3 um)", + "Thermal-infrared (11.5-12.5 um)" + }); + + imageTypes.append(channelTypes[m_tempImage.chA]); + imageTypes.append(channelTypes[m_tempImage.chB]); + } + + // Crop noise due to low elevation at top and bottom of image + if (m_settings.m_cropNoise) + m_tempImage.zenith -= apt_cropNoise(&m_tempImage); + + // Denoise filter + if (m_settings.m_denoise) + { + apt_denoise(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + apt_denoise(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + } + + // Flip image if satellite pass is North to South + if (m_settings.m_flip) + { + apt_flipImage(&m_tempImage, APT_CH_WIDTH, APT_CHA_OFFSET); + apt_flipImage(&m_tempImage, APT_CH_WIDTH, APT_CHB_OFFSET); + } + + // Linear equalise to improve contrast + if (m_settings.m_linearEqualise) + { + apt_linearEnhance(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + apt_linearEnhance(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + } + + // Histogram equalise to improve contrast + if (m_settings.m_histogramEqualise) + { + apt_histogramEqualise(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + apt_histogramEqualise(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + } + + if (m_settings.m_precipitationOverlay) + { + // Overlay precipitation + for (int r = 0; r < m_tempImage.nrow; r++) + { + uchar *l = m_colourImage.scanLine(r); + for (int i = 0; i < APT_IMG_WIDTH; i++) + { + float p = m_tempImage.prow[r][i]; + + if ((i >= APT_CHB_OFFSET) && (i < APT_CHB_OFFSET + APT_CH_WIDTH) && (p >= 198)) + { + apt_rgb_t rgb = apt_applyPalette(apt_PrecipPalette, p - 198); + // Negative float values get converted to positive uchars here + l[i*3] = (uchar)rgb.r; + l[i*3+1] = (uchar)rgb.g; + l[i*3+2] = (uchar)rgb.b; + int a = i - APT_CHB_OFFSET + APT_CHA_OFFSET; + l[a*3] = (uchar)rgb.r; + l[a*3+1] = (uchar)rgb.g; + l[a*3+2] = (uchar)rgb.b; + } + else + { + uchar q = roundAndClip(p); + l[i*3] = q; + l[i*3+1] = q; + l[i*3+2] = q; + } + } + } + return extractImage(m_colourImage); + } + else + { + for (int r = 0; r < m_tempImage.nrow; r++) + { + uchar *l = m_greyImage.scanLine(r); + + for (int i = 0; i < APT_IMG_WIDTH; i++) + { + float p = m_tempImage.prow[r][i]; + l[i] = roundAndClip(p); + } + } + return extractImage(m_greyImage); + } +} + +QImage APTDemodImageWorker::extractImage(QImage image) +{ + if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) { + return image.copy(0, 0, APT_IMG_WIDTH, m_tempImage.nrow); + } else if (m_settings.m_channels == APTDemodSettings::CHANNEL_A) { + return image.copy(APT_CHA_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow); + } else { + return image.copy(APT_CHB_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow); + } +} + +void APTDemodImageWorker::copyImage(apt_image_t *dst, apt_image_t *src) +{ + dst->nrow = src->nrow; + dst->zenith = src->zenith; + dst->chA = src->chA; + dst->chB = src->chB; + + for (int i = 0; i < src->nrow; i++) { + std::copy(src->prow[i], src->prow[i] + APT_PROW_WIDTH, dst->prow[i]); + } +} + +uchar APTDemodImageWorker::roundAndClip(float p) +{ + int q = (int) round(p); + q = q > 255 ? 255 : q < 0 ? 0 : q; + return q; +} diff --git a/plugins/channelrx/demodapt/aptdemodimageworker.h b/plugins/channelrx/demodapt/aptdemodimageworker.h new file mode 100644 index 000000000..2b7db866d --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodimageworker.h @@ -0,0 +1,100 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 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_APTDEMODIMAGEWORKER_H +#define INCLUDE_APTDEMODIMAGEWORKER_H + +#include +#include +#include + +#include + +#include "util/messagequeue.h" +#include "util/message.h" + +#include "aptdemodsettings.h" + +class APTDemodImageWorker : public QObject +{ + Q_OBJECT +public: + class MsgConfigureAPTDemodImageWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const APTDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAPTDemodImageWorker* create(const APTDemodSettings& settings, bool force) + { + return new MsgConfigureAPTDemodImageWorker(settings, force); + } + + private: + APTDemodSettings m_settings; + bool m_force; + + MsgConfigureAPTDemodImageWorker(const APTDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + APTDemodImageWorker(); + ~APTDemodImageWorker(); + void reset(); + void startWork(); + void stopWork(); + bool isRunning() const { return m_running; } + + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_messageQueueToGUI = messageQueue; } + +private: + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_messageQueueToGUI; + APTDemodSettings m_settings; + + // Image buffers + apt_image_t m_image; // Received image + apt_image_t m_tempImage; // Processed image + QImage m_greyImage; + QImage m_colourImage; + QString m_satelliteName; + + bool m_running; + QMutex m_mutex; + + bool handleMessage(const Message& cmd); + void applySettings(const APTDemodSettings& settings, bool force = false); + void resetDecoder(); + void processPixels(const float *pixels); + void sendImageToGUI(); + QImage processImage(QStringList& imageTypes); + QImage extractImage(QImage image); + + static void copyImage(apt_image_t *dst, apt_image_t *src); + static uchar roundAndClip(float p); + +private slots: + void handleInputMessages(); +}; + +#endif // INCLUDE_APTDEMODIMAGEWORKER_H diff --git a/plugins/channelrx/demodapt/aptdemodsink.cpp b/plugins/channelrx/demodapt/aptdemodsink.cpp index 38538dc2f..f84a5dc0f 100644 --- a/plugins/channelrx/demodapt/aptdemodsink.cpp +++ b/plugins/channelrx/demodapt/aptdemodsink.cpp @@ -37,7 +37,7 @@ APTDemodSink::APTDemodSink(APTDemod *packetDemod) : m_magsqSum(0.0f), m_magsqPeak(0.0f), m_magsqCount(0), - m_messageQueueToChannel(nullptr), + m_imageWorkerMessageQueue(nullptr), m_samples(nullptr) { m_magsq = 0.0; @@ -124,7 +124,11 @@ void APTDemodSink::feed(const SampleVector::const_iterator& begin, const SampleV { float pixels[APT_PROW_WIDTH]; apt_getpixelrow(pixels, m_row, &m_zenith, m_row == 0, getsamples, this); - getMessageQueueToChannel()->push(APTDemod::MsgPixels::create(pixels, m_zenith)); + + if (getImageWorkerMessageQueue()) { + getImageWorkerMessageQueue()->push(APTDemod::MsgPixels::create(pixels, m_zenith)); + } + m_row++; } } diff --git a/plugins/channelrx/demodapt/aptdemodsink.h b/plugins/channelrx/demodapt/aptdemodsink.h index 9a8a14284..e65f903e5 100644 --- a/plugins/channelrx/demodapt/aptdemodsink.h +++ b/plugins/channelrx/demodapt/aptdemodsink.h @@ -51,7 +51,7 @@ public: void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); void applySettings(const APTDemodSettings& settings, bool force = false); - void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setImageWorkerMessageQueue(MessageQueue *messageQueue) { m_imageWorkerMessageQueue = messageQueue; } double getMagSq() const { return m_magsq; } @@ -103,7 +103,7 @@ private: int m_magsqCount; MagSqLevelsStore m_magSqLevelStore; - MessageQueue *m_messageQueueToChannel; + MessageQueue *m_imageWorkerMessageQueue; MovingAverageUtil m_movingAverage; @@ -120,7 +120,7 @@ private: int m_zenith; // Row number of Zenith void processOneSample(Complex &ci); - MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + MessageQueue *getImageWorkerMessageQueue() { return m_imageWorkerMessageQueue; } }; #endif // INCLUDE_APTDEMODSINK_H