Erster Teststream absolviert. Positives Ergebnis
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(HDMIViewer VERSION 1.3.2 LANGUAGES CXX)
|
project(HDMIViewer VERSION 1.4.0 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -8,7 +8,8 @@ set(CMAKE_AUTORCC ON)
|
|||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
|
|
||||||
# Qt6 – Gui enthält den JPEG-Plugin-Support (QImage::fromData)
|
# Qt6 – Gui enthält den JPEG-Plugin-Support (QImage::fromData)
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
|
# OpenGLWidgets für das GPU-beschleunigte Video-Rendering (Bildglättung/Retro-Optik)
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets OpenGL OpenGLWidgets)
|
||||||
|
|
||||||
# ALSA
|
# ALSA
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -37,6 +38,8 @@ target_link_libraries(hdmi-viewer
|
|||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
|
Qt6::OpenGL
|
||||||
|
Qt6::OpenGLWidgets
|
||||||
${ALSA_LIBRARIES}
|
${ALSA_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -14,6 +14,32 @@ Gedacht als Companion zu OBS Studio: **Anzeigen ohne Aufnehmen**.
|
|||||||
- Titelleiste zeigt tatsächliche Auflösung und FPS
|
- Titelleiste zeigt tatsächliche Auflösung und FPS
|
||||||
- Colorspace-Konvertierung im ThreadPool (kein Ruckeln)
|
- Colorspace-Konvertierung im ThreadPool (kein Ruckeln)
|
||||||
- FPS explizit auf 60fps gesetzt (was Grabber liefert wird in Statusbar angezeigt)
|
- FPS explizit auf 60fps gesetzt (was Grabber liefert wird in Statusbar angezeigt)
|
||||||
|
- **Bildglättung** (standardmäßig an): GPU-Shader trennt Luma/Chroma und
|
||||||
|
behandelt sie getrennt – Chroma wird stärker weichgezeichnet (killt
|
||||||
|
Dot-Crawl/Farbrauschen billiger Composite→HDMI-Scaler wie Lioncast Mini
|
||||||
|
AV2HDMI o.ä.), Luma wird leicht geglättet und dezent gegengeschärft
|
||||||
|
(nimmt harte Scaler-Kanten raus, ohne matschig zu wirken). Keine Optik-
|
||||||
|
Spielerei, sondern eine echte Bildverbesserung. Taste **S** zum Umschalten.
|
||||||
|
- **Retro-Optik** (standardmäßig aus): separater Nostalgie-Look – Scanlines,
|
||||||
|
Phosphor-/Aperture-Grille-Maske, leichte Wölbung wie bei einem alten
|
||||||
|
Röhrenfernseher. Lässt sich mit der Bildglättung kombinieren. Taste **C**.
|
||||||
|
- **Helligkeit** (Tasten **,** dunkler / **.** heller): Gamma-Anpassung, live
|
||||||
|
und persistiert. Sinnvoll als Feinjustierung, falls Farbraum-Erkennung
|
||||||
|
(siehe unten) allein nicht perfekt zum eigenen Bildschirm passt.
|
||||||
|
- **Hardware-Helligkeit**: Beim Öffnen des Geräts wird (falls vom Chip
|
||||||
|
unterstützt) der V4L2-Brightness-Regler direkt am Grabber moderat
|
||||||
|
angehoben (+20 % des Wertebereichs ab Werkseinstellung) – das wirkt
|
||||||
|
bereits VOR der JPEG-Kompression und kann so tatsächlich verlorene
|
||||||
|
Schattendetails retten, statt nur nachträglich zu strecken, was von der
|
||||||
|
Kompression übrig blieb. Diagnose-Ausgabe der verfügbaren Regler landet
|
||||||
|
beim Start im Terminal.
|
||||||
|
- **Studio-/Full-Range-Korrektur**: Das fertig dekodierte Bild wird von einem
|
||||||
|
komprimierten Kontrastumfang (Schwarzpunkt niedrig angesetzt, Weißpunkt
|
||||||
|
≈235/255) auf den vollen 0–255-Kontrastumfang gedehnt. Ohne diese
|
||||||
|
Korrektur wirkt das Bild dumpf, kontrastarm und zu dunkel – viele billige
|
||||||
|
HDMI-Grabber liefern (oder kodieren, auch bei MJPEG) genau in diesem
|
||||||
|
komprimierten Bereich. Greift direkt im GPU-Shader auf dem fertigen
|
||||||
|
RGB-Bild, also unabhängig vom Pixelformat.
|
||||||
|
|
||||||
## Abhängigkeiten (Arch Linux)
|
## Abhängigkeiten (Arch Linux)
|
||||||
|
|
||||||
@@ -21,6 +47,9 @@ Gedacht als Companion zu OBS Studio: **Anzeigen ohne Aufnehmen**.
|
|||||||
sudo pacman -S qt6-base cmake alsa-lib linux-headers base-devel
|
sudo pacman -S qt6-base cmake alsa-lib linux-headers base-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`qt6-base` bringt auf Arch bereits die benötigten `Qt6OpenGL`- und
|
||||||
|
`Qt6OpenGLWidgets`-Module mit.
|
||||||
|
|
||||||
## Bauen
|
## Bauen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -40,6 +69,9 @@ make -j$(nproc)
|
|||||||
| Taste | Funktion |
|
| Taste | Funktion |
|
||||||
|--------------|---------------------------------------------|
|
|--------------|---------------------------------------------|
|
||||||
| F | Vollbild ein/aus |
|
| F | Vollbild ein/aus |
|
||||||
|
| S | Bildglättung ein/aus |
|
||||||
|
| C | Retro-Optik (Scanlines/Maske) ein/aus |
|
||||||
|
| , / . | Dunkler / Heller (Helligkeit, gespeichert) |
|
||||||
| Esc | Vollbild verlassen ODER zurück zum Dialog |
|
| Esc | Vollbild verlassen ODER zurück zum Dialog |
|
||||||
|
|
||||||
## Einstellungen
|
## Einstellungen
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#include "CaptureThread.h"
|
#include "CaptureThread.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -16,6 +20,48 @@ static int xioctl(int fd, unsigned long request, void* arg)
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listet alle vom Grabber-Chip unterstützten V4L2-Regler (Helligkeit,
|
||||||
|
// Kontrast, Gain, Belichtung, ...) mit ihrem Wertebereich und aktuellen Wert
|
||||||
|
// ins Terminal. Rein diagnostisch – zeigt, ob/welche Hardware-Verstärkung
|
||||||
|
// VOR der JPEG-Kompression überhaupt ansetzbar wäre, bevor wir dafür UI
|
||||||
|
// bauen. Manche Chips unterstützen keinen dieser Regler; dann bleibt die
|
||||||
|
// Liste leer, was ebenfalls eine wichtige Information ist.
|
||||||
|
static void logAvailableControls(int fd)
|
||||||
|
{
|
||||||
|
qDebug().noquote() << "── V4L2-Regler des Grabbers ──────────────────────";
|
||||||
|
|
||||||
|
v4l2_queryctrl qc{};
|
||||||
|
qc.id = V4L2_CTRL_FLAG_NEXT_CTRL;
|
||||||
|
bool any = false;
|
||||||
|
|
||||||
|
while (xioctl(fd, VIDIOC_QUERYCTRL, &qc) == 0) {
|
||||||
|
if (!(qc.flags & V4L2_CTRL_FLAG_DISABLED) &&
|
||||||
|
qc.type != V4L2_CTRL_TYPE_CTRL_CLASS) {
|
||||||
|
any = true;
|
||||||
|
|
||||||
|
v4l2_control ctrl{};
|
||||||
|
ctrl.id = qc.id;
|
||||||
|
const bool gotCurrent = (xioctl(fd, VIDIOC_G_CTRL, &ctrl) == 0);
|
||||||
|
|
||||||
|
qDebug().noquote() << QString(
|
||||||
|
" %1 (id=0x%2) min=%3 max=%4 step=%5 default=%6 aktuell=%7")
|
||||||
|
.arg(QString::fromLatin1(reinterpret_cast<const char*>(qc.name)), -28)
|
||||||
|
.arg(qc.id, 0, 16)
|
||||||
|
.arg(qc.minimum)
|
||||||
|
.arg(qc.maximum)
|
||||||
|
.arg(qc.step)
|
||||||
|
.arg(qc.default_value)
|
||||||
|
.arg(gotCurrent ? QString::number(ctrl.value) : QStringLiteral("?"));
|
||||||
|
}
|
||||||
|
qc.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any)
|
||||||
|
qDebug().noquote() << " (keine regelbaren Controls gemeldet)";
|
||||||
|
|
||||||
|
qDebug().noquote() << "───────────────────────────────────────────────────";
|
||||||
|
}
|
||||||
|
|
||||||
// ── YUYV → RGB888 (BT.601 fixed-point) ──────────────────────────────────────
|
// ── YUYV → RGB888 (BT.601 fixed-point) ──────────────────────────────────────
|
||||||
|
|
||||||
static inline quint8 clamp255(int v)
|
static inline quint8 clamp255(int v)
|
||||||
@@ -40,6 +86,11 @@ void CaptureThread::stopAndWait()
|
|||||||
wait();
|
wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CaptureThread::requestFrameDump()
|
||||||
|
{
|
||||||
|
m_dumpRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
void CaptureThread::run()
|
void CaptureThread::run()
|
||||||
{
|
{
|
||||||
if (!openDevice() || !initDevice() || !setFramerate() || !startCapture()) {
|
if (!openDevice() || !initDevice() || !setFramerate() || !startCapture()) {
|
||||||
@@ -117,6 +168,10 @@ bool CaptureThread::openDevice()
|
|||||||
emit errorOccurred(m_device + " unterstützt kein Streaming");
|
emit errorOccurred(m_device + " unterstützt kein Streaming");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logAvailableControls(m_fd);
|
||||||
|
applyHardwareControls();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,14 +328,131 @@ void CaptureThread::closeDevice()
|
|||||||
m_buffers.clear();
|
m_buffers.clear();
|
||||||
|
|
||||||
if (m_fd != -1) {
|
if (m_fd != -1) {
|
||||||
|
restoreHardwareControls();
|
||||||
::close(m_fd);
|
::close(m_fd);
|
||||||
m_fd = -1;
|
m_fd = -1;
|
||||||
qDebug() << "V4L2 freigegeben:" << m_device;
|
qDebug() << "V4L2 freigegeben:" << m_device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setzt beim Start Helligkeit/Kontrast/Sättigung des Grabber-Chips auf die
|
||||||
|
// vom Nutzer konfigurierten Werte (persistiert in QSettings, per Tasten
|
||||||
|
// oder Startdialog gesetzt) – VOR der JPEG-Kompression, was tatsächlich
|
||||||
|
// Schattendetails vor dem verlustbehafteten Encoding retten kann. Merkt
|
||||||
|
// sich dabei den JEWEILS AKTUELLEN Wert jedes Reglers (der Ausgangszustand,
|
||||||
|
// egal von wem/wann gesetzt), um ihn beim Beenden wiederherzustellen – so
|
||||||
|
// hinterlässt hdmi-viewer den Chip nicht dauerhaft verändert für andere
|
||||||
|
// Programme. Regler ohne gespeicherten Nutzerwert (settings.contains ==
|
||||||
|
// false) werden gar nicht erst angefasst.
|
||||||
|
void CaptureThread::applyHardwareControls()
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
|
||||||
|
auto handle = [&](quint32 id, const char* key, int& restoreOut) {
|
||||||
|
v4l2_queryctrl qc{};
|
||||||
|
qc.id = id;
|
||||||
|
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
|
||||||
|
return;
|
||||||
|
|
||||||
|
v4l2_control cur{};
|
||||||
|
cur.id = id;
|
||||||
|
if (xioctl(m_fd, VIDIOC_G_CTRL, &cur) == 0)
|
||||||
|
restoreOut = cur.value;
|
||||||
|
|
||||||
|
if (!settings.contains(key))
|
||||||
|
return; // kein expliziter Nutzerwert -> unangetastet lassen
|
||||||
|
|
||||||
|
const int target = qBound(qc.minimum, settings.value(key).toInt(), qc.maximum);
|
||||||
|
v4l2_control ctrl{};
|
||||||
|
ctrl.id = id;
|
||||||
|
ctrl.value = target;
|
||||||
|
if (xioctl(m_fd, VIDIOC_S_CTRL, &ctrl) == 0) {
|
||||||
|
qDebug().noquote() << QString("%1 gesetzt: %2 (Ausgangswert %3 gemerkt)")
|
||||||
|
.arg(key).arg(target).arg(restoreOut);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handle(V4L2_CID_BRIGHTNESS, "hardware/brightness", m_restoreBrightness);
|
||||||
|
handle(V4L2_CID_CONTRAST, "hardware/contrast", m_restoreContrast);
|
||||||
|
handle(V4L2_CID_SATURATION, "hardware/saturation", m_restoreSaturation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaptureThread::restoreHardwareControls()
|
||||||
|
{
|
||||||
|
auto restore = [&](quint32 id, int value) {
|
||||||
|
if (value < 0) return;
|
||||||
|
v4l2_control ctrl{};
|
||||||
|
ctrl.id = id;
|
||||||
|
ctrl.value = value;
|
||||||
|
xioctl(m_fd, VIDIOC_S_CTRL, &ctrl);
|
||||||
|
};
|
||||||
|
restore(V4L2_CID_BRIGHTNESS, m_restoreBrightness);
|
||||||
|
restore(V4L2_CID_CONTRAST, m_restoreContrast);
|
||||||
|
restore(V4L2_CID_SATURATION, m_restoreSaturation);
|
||||||
|
qDebug().noquote() << "Hardware-Regler auf Ausgangswerte zurückgesetzt.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für Live-Tasten (Kontrast/Sättigung): liest den aktuellen Wert, addiert
|
||||||
|
// delta, schreibt zurück, merkt sich den neuen Wert dauerhaft und meldet
|
||||||
|
// ihn per Signal (für die Einblendung in der UI).
|
||||||
|
void CaptureThread::adjustHardwareControl(quint32 id, const char* settingsKey,
|
||||||
|
const QString& label, int delta)
|
||||||
|
{
|
||||||
|
if (m_fd == -1) return;
|
||||||
|
|
||||||
|
v4l2_queryctrl qc{};
|
||||||
|
qc.id = id;
|
||||||
|
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
|
||||||
|
return;
|
||||||
|
|
||||||
|
v4l2_control cur{};
|
||||||
|
cur.id = id;
|
||||||
|
if (xioctl(m_fd, VIDIOC_G_CTRL, &cur) == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int next = qBound(qc.minimum, cur.value + delta, qc.maximum);
|
||||||
|
v4l2_control ctrl{};
|
||||||
|
ctrl.id = id;
|
||||||
|
ctrl.value = next;
|
||||||
|
|
||||||
|
if (xioctl(m_fd, VIDIOC_S_CTRL, &ctrl) == 0) {
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue(settingsKey, next);
|
||||||
|
emit hardwareControlChanged(label, next, qc.minimum, qc.maximum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaptureThread::adjustHardwareContrast(int delta)
|
||||||
|
{
|
||||||
|
adjustHardwareControl(V4L2_CID_CONTRAST, "hardware/contrast", "Hardware-Kontrast", delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaptureThread::adjustHardwareSaturation(int delta)
|
||||||
|
{
|
||||||
|
adjustHardwareControl(V4L2_CID_SATURATION, "hardware/saturation", "Hardware-Sättigung", delta);
|
||||||
|
}
|
||||||
|
|
||||||
void CaptureThread::processFrame(const void* data, size_t size)
|
void CaptureThread::processFrame(const void* data, size_t size)
|
||||||
{
|
{
|
||||||
|
// Diagnose: rohes MJPEG-Frame unverändert (Byte für Byte, wie vom Chip
|
||||||
|
// geliefert) auf die Platte speichern, BEVOR irgendeine Dekodierung/
|
||||||
|
// Verarbeitung stattfindet. Lässt sich in einem unabhängigen Bildbe-
|
||||||
|
// trachter öffnen, um zu prüfen, ob die Rohdaten selbst schon dunkel
|
||||||
|
// sind oder ob die Abweichung erst bei uns beim Dekodieren entsteht.
|
||||||
|
if (m_dumpRequested.exchange(false) && m_pixelFormat == V4L2_PIX_FMT_MJPEG) {
|
||||||
|
const QString path = QDir::homePath() + "/hdmiviewer_frame_dump_" +
|
||||||
|
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss") + ".jpg";
|
||||||
|
QFile f(path);
|
||||||
|
if (f.open(QIODevice::WriteOnly)) {
|
||||||
|
f.write(reinterpret_cast<const char*>(data), static_cast<qint64>(size));
|
||||||
|
f.close();
|
||||||
|
qDebug().noquote() << "Rohes Frame gespeichert:" << path;
|
||||||
|
emit frameDumped(path);
|
||||||
|
} else {
|
||||||
|
qDebug().noquote() << "Frame-Dump fehlgeschlagen: Datei konnte nicht geschrieben werden.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QImage img;
|
QImage img;
|
||||||
switch (m_pixelFormat) {
|
switch (m_pixelFormat) {
|
||||||
case V4L2_PIX_FMT_MJPEG: img = decodeMJPEG(data, size); break;
|
case V4L2_PIX_FMT_MJPEG: img = decodeMJPEG(data, size); break;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public:
|
|||||||
~CaptureThread() override;
|
~CaptureThread() override;
|
||||||
|
|
||||||
void stopAndWait();
|
void stopAndWait();
|
||||||
|
void requestFrameDump(); // speichert das nächste rohe MJPEG-Frame unverändert auf die Platte
|
||||||
|
void adjustHardwareContrast(int delta);
|
||||||
|
void adjustHardwareSaturation(int delta);
|
||||||
|
|
||||||
int captureWidth() const { return m_width; }
|
int captureWidth() const { return m_width; }
|
||||||
int captureHeight() const { return m_height; }
|
int captureHeight() const { return m_height; }
|
||||||
@@ -32,6 +35,8 @@ signals:
|
|||||||
void frameReady(QImage image);
|
void frameReady(QImage image);
|
||||||
void errorOccurred(const QString& message);
|
void errorOccurred(const QString& message);
|
||||||
void captureInfo(int width, int height, int fps, const QString& format);
|
void captureInfo(int width, int height, int fps, const QString& format);
|
||||||
|
void frameDumped(QString path);
|
||||||
|
void hardwareControlChanged(QString label, int value, int minVal, int maxVal);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void run() override;
|
void run() override;
|
||||||
@@ -45,6 +50,9 @@ private:
|
|||||||
void closeDevice();
|
void closeDevice();
|
||||||
bool initMmap();
|
bool initMmap();
|
||||||
void processFrame(const void* data, size_t size);
|
void processFrame(const void* data, size_t size);
|
||||||
|
void applyHardwareControls(); // beim Start: eigene Werte setzen, Ausgangswerte merken
|
||||||
|
void restoreHardwareControls(); // beim Beenden: Ausgangswerte wiederherstellen
|
||||||
|
void adjustHardwareControl(quint32 id, const char* settingsKey, const QString& label, int delta);
|
||||||
|
|
||||||
// Dekodierung
|
// Dekodierung
|
||||||
QImage decodeYUYV(const void* data) const;
|
QImage decodeYUYV(const void* data) const;
|
||||||
@@ -60,4 +68,11 @@ private:
|
|||||||
quint32 m_pixelFormat = V4L2_PIX_FMT_MJPEG;
|
quint32 m_pixelFormat = V4L2_PIX_FMT_MJPEG;
|
||||||
std::vector<Buffer> m_buffers;
|
std::vector<Buffer> m_buffers;
|
||||||
std::atomic<bool> m_running{false};
|
std::atomic<bool> m_running{false};
|
||||||
|
std::atomic<bool> m_dumpRequested{false};
|
||||||
|
|
||||||
|
// Ausgangswerte der Hardware-Regler (vor unseren Änderungen), für die
|
||||||
|
// Wiederherstellung beim Beenden. -1 = nicht gelesen/nicht verfügbar.
|
||||||
|
int m_restoreBrightness = -1;
|
||||||
|
int m_restoreContrast = -1;
|
||||||
|
int m_restoreSaturation = -1;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
#include "DeviceDialog.h"
|
#include "DeviceDialog.h"
|
||||||
|
#include "VideoWidget.h" // für kBrightnessMin/Max, kSharpenMin/Max (eine Quelle der Wahrheit)
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QDoubleSpinBox>
|
||||||
|
#include <QSpinBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@@ -51,11 +55,79 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
|||||||
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
|
||||||
form->addRow("Ziel-Framerate:", m_fpsCombo);
|
form->addRow("Ziel-Framerate:", m_fpsCombo);
|
||||||
|
|
||||||
|
m_smoothCheck = new QCheckBox("Bildglättung aktivieren (empfohlen)", this);
|
||||||
|
m_smoothCheck->setChecked(true);
|
||||||
|
m_smoothCheck->setToolTip(
|
||||||
|
"Reduziert live Kompressions-/Scaler-Artefakte deines HDMI-Upscalers:\n"
|
||||||
|
"Farbrauschen/Dot-Crawl wird weicher gezeichnet, harte blockige\n"
|
||||||
|
"Pixelkanten werden geglättet und dezent gegengeschärft.\n"
|
||||||
|
"Das ist keine Optik-Spielerei, sondern eine Bildverbesserung –\n"
|
||||||
|
"empfohlen für Composite-Quellen wie PS1/PS2 an billigen Scalern.\n"
|
||||||
|
"Umschaltbar im laufenden Betrieb mit [S].");
|
||||||
|
form->addRow("", m_smoothCheck);
|
||||||
|
|
||||||
|
m_retroCheck = new QCheckBox("Retro-Optik (Scanlines/Maske) aktivieren", this);
|
||||||
|
m_retroCheck->setToolTip(
|
||||||
|
"Rein optischer Nostalgie-Look: Scanlines, Phosphor-Maske,\n"
|
||||||
|
"leichte Wölbung wie bei einem alten Röhrenfernseher.\n"
|
||||||
|
"Lässt sich mit der Bildglättung kombinieren.\n"
|
||||||
|
"Umschaltbar im laufenden Betrieb mit [C].");
|
||||||
|
form->addRow("", m_retroCheck);
|
||||||
|
|
||||||
|
m_sharpenSpin = new QDoubleSpinBox(this);
|
||||||
|
m_sharpenSpin->setRange(VideoWidget::kSharpenMin, VideoWidget::kSharpenMax);
|
||||||
|
m_sharpenSpin->setSingleStep(VideoWidget::kSharpenStep);
|
||||||
|
m_sharpenSpin->setDecimals(1);
|
||||||
|
m_sharpenSpin->setValue(0.0);
|
||||||
|
m_sharpenSpin->setSpecialValueText("aus");
|
||||||
|
m_sharpenSpin->setToolTip(
|
||||||
|
"Unsharp Mask übers gesamte Bild, unabhängig von der Bildglättung.\n"
|
||||||
|
"0 = aus. Im laufenden Betrieb mit [X]/[+]/[-] änderbar.");
|
||||||
|
form->addRow("Nachschärfen:", m_sharpenSpin);
|
||||||
|
|
||||||
|
m_brightnessSpin = new QDoubleSpinBox(this);
|
||||||
|
m_brightnessSpin->setRange(VideoWidget::kBrightnessMin * 100.0, VideoWidget::kBrightnessMax * 100.0);
|
||||||
|
m_brightnessSpin->setSingleStep(VideoWidget::kBrightnessStep * 100.0);
|
||||||
|
m_brightnessSpin->setDecimals(0);
|
||||||
|
m_brightnessSpin->setSuffix(" %");
|
||||||
|
m_brightnessSpin->setValue(100.0);
|
||||||
|
m_brightnessSpin->setToolTip(
|
||||||
|
"Gamma-Helligkeit, 100 % = unverändert. Im laufenden Betrieb\n"
|
||||||
|
"mit [,]/[.] änderbar.");
|
||||||
|
form->addRow("Helligkeit:", m_brightnessSpin);
|
||||||
|
|
||||||
|
m_hwContrastSpin = new QSpinBox(this);
|
||||||
|
m_hwContrastSpin->setRange(-1, 100);
|
||||||
|
m_hwContrastSpin->setValue(-1);
|
||||||
|
m_hwContrastSpin->setSpecialValueText("unverändert");
|
||||||
|
m_hwContrastSpin->setToolTip(
|
||||||
|
"Hardware-Kontrast direkt am Grabber-Chip, VOR der JPEG-Kompression\n"
|
||||||
|
"(Wertebereich hängt vom jeweiligen Chip ab, hier 0-100 angenommen).\n"
|
||||||
|
"\"unverändert\" lässt den aktuellen Chip-Wert einfach in Ruhe.\n"
|
||||||
|
"Im laufenden Betrieb mit [1]/[2] änderbar. hdmi-viewer merkt sich\n"
|
||||||
|
"beim Start den Ausgangswert und stellt ihn beim Beenden wieder her.");
|
||||||
|
form->addRow("Hardware-Kontrast:", m_hwContrastSpin);
|
||||||
|
|
||||||
|
m_hwSaturationSpin = new QSpinBox(this);
|
||||||
|
m_hwSaturationSpin->setRange(-1, 100);
|
||||||
|
m_hwSaturationSpin->setValue(-1);
|
||||||
|
m_hwSaturationSpin->setSpecialValueText("unverändert");
|
||||||
|
m_hwSaturationSpin->setToolTip(
|
||||||
|
"Hardware-Sättigung direkt am Grabber-Chip, VOR der JPEG-Kompression.\n"
|
||||||
|
"\"unverändert\" lässt den aktuellen Chip-Wert einfach in Ruhe.\n"
|
||||||
|
"Im laufenden Betrieb mit [3]/[4] änderbar. hdmi-viewer merkt sich\n"
|
||||||
|
"beim Start den Ausgangswert und stellt ihn beim Beenden wieder her.");
|
||||||
|
form->addRow("Hardware-Sättigung:", m_hwSaturationSpin);
|
||||||
|
|
||||||
auto* hint = new QLabel(
|
auto* hint = new QLabel(
|
||||||
"<small><b>Hinweis:</b> Der Grabber liefert die gewünschte FPS-Zahl nur wenn "
|
"<small><b>Hinweis:</b> Der Grabber liefert die gewünschte FPS-Zahl nur wenn "
|
||||||
"genug USB-Bandbreite vorhanden ist. Die tatsächliche Rate wird in der "
|
"genug USB-Bandbreite vorhanden ist. Die tatsächliche Rate wird in der "
|
||||||
"Titelleiste angezeigt.<br>"
|
"Titelleiste angezeigt.<br>"
|
||||||
"<b>F</b> = Vollbild · <b>Esc</b> = zurück hierher</small>",
|
"<b>F</b> Vollbild · <b>S</b> Glättung · "
|
||||||
|
"<b>C</b> Retro · <b>X/+/-</b> Schärfen · "
|
||||||
|
"<b>,/.</b> Helligkeit · <b>1/2</b> HW-Kontrast · "
|
||||||
|
"<b>3/4</b> HW-Sättigung · <b>V</b> Vergleich · "
|
||||||
|
"<b>P</b> Screenshot · <b>Esc</b> zurück hierher</small>",
|
||||||
this);
|
this);
|
||||||
hint->setWordWrap(true);
|
hint->setWordWrap(true);
|
||||||
|
|
||||||
@@ -81,10 +153,22 @@ DeviceDialog::DeviceDialog(QWidget* parent)
|
|||||||
QString DeviceDialog::selectedVideoDevice() const { return m_videoCombo->currentData().toString(); }
|
QString DeviceDialog::selectedVideoDevice() const { return m_videoCombo->currentData().toString(); }
|
||||||
QString DeviceDialog::selectedAudioDevice() const { return m_audioCombo->currentData().toString(); }
|
QString DeviceDialog::selectedAudioDevice() const { return m_audioCombo->currentData().toString(); }
|
||||||
int DeviceDialog::selectedFps() const { return m_fpsCombo->currentData().toInt(); }
|
int DeviceDialog::selectedFps() const { return m_fpsCombo->currentData().toInt(); }
|
||||||
|
bool DeviceDialog::selectedSmoothMode() const { return m_smoothCheck->isChecked(); }
|
||||||
|
bool DeviceDialog::selectedRetroMode() const { return m_retroCheck->isChecked(); }
|
||||||
|
float DeviceDialog::selectedSharpenAmount() const { return static_cast<float>(m_sharpenSpin->value()); }
|
||||||
|
float DeviceDialog::selectedBrightness() const { return static_cast<float>(m_brightnessSpin->value() / 100.0); }
|
||||||
|
int DeviceDialog::selectedHwContrast() const { return m_hwContrastSpin->value(); }
|
||||||
|
int DeviceDialog::selectedHwSaturation() const { return m_hwSaturationSpin->value(); }
|
||||||
|
|
||||||
void DeviceDialog::setPreselectedVideo(const QString& device) { selectByData(m_videoCombo, device); }
|
void DeviceDialog::setPreselectedVideo(const QString& device) { selectByData(m_videoCombo, device); }
|
||||||
void DeviceDialog::setPreselectedAudio(const QString& device) { selectByData(m_audioCombo, device); }
|
void DeviceDialog::setPreselectedAudio(const QString& device) { selectByData(m_audioCombo, device); }
|
||||||
void DeviceDialog::setPreselectedFps(int fps) { selectByData(m_fpsCombo, QString::number(fps)); }
|
void DeviceDialog::setPreselectedFps(int fps) { selectByData(m_fpsCombo, QString::number(fps)); }
|
||||||
|
void DeviceDialog::setPreselectedSmoothMode(bool enabled) { m_smoothCheck->setChecked(enabled); }
|
||||||
|
void DeviceDialog::setPreselectedRetroMode(bool enabled) { m_retroCheck->setChecked(enabled); }
|
||||||
|
void DeviceDialog::setPreselectedSharpenAmount(float amount) { m_sharpenSpin->setValue(amount); }
|
||||||
|
void DeviceDialog::setPreselectedBrightness(float gamma) { m_brightnessSpin->setValue(gamma * 100.0); }
|
||||||
|
void DeviceDialog::setPreselectedHwContrast(int value) { m_hwContrastSpin->setValue(value); }
|
||||||
|
void DeviceDialog::setPreselectedHwSaturation(int value) { m_hwSaturationSpin->setValue(value); }
|
||||||
|
|
||||||
void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
|
void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
|
class QCheckBox;
|
||||||
|
class QDoubleSpinBox;
|
||||||
|
class QSpinBox;
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class DeviceDialog : public QDialog
|
class DeviceDialog : public QDialog
|
||||||
@@ -17,10 +20,22 @@ public:
|
|||||||
QString selectedVideoDevice() const;
|
QString selectedVideoDevice() const;
|
||||||
QString selectedAudioDevice() const;
|
QString selectedAudioDevice() const;
|
||||||
int selectedFps() const;
|
int selectedFps() const;
|
||||||
|
bool selectedSmoothMode() const;
|
||||||
|
bool selectedRetroMode() const;
|
||||||
|
float selectedSharpenAmount() const;
|
||||||
|
float selectedBrightness() const;
|
||||||
|
int selectedHwContrast() const; // -1 = unverändert lassen
|
||||||
|
int selectedHwSaturation() const; // -1 = unverändert lassen
|
||||||
|
|
||||||
void setPreselectedVideo(const QString& device);
|
void setPreselectedVideo(const QString& device);
|
||||||
void setPreselectedAudio(const QString& device);
|
void setPreselectedAudio(const QString& device);
|
||||||
void setPreselectedFps(int fps);
|
void setPreselectedFps(int fps);
|
||||||
|
void setPreselectedSmoothMode(bool enabled);
|
||||||
|
void setPreselectedRetroMode(bool enabled);
|
||||||
|
void setPreselectedSharpenAmount(float amount);
|
||||||
|
void setPreselectedBrightness(float gamma);
|
||||||
|
void setPreselectedHwContrast(int value); // -1 = unverändert
|
||||||
|
void setPreselectedHwSaturation(int value); // -1 = unverändert
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void populateVideoDevices();
|
void populateVideoDevices();
|
||||||
@@ -30,4 +45,10 @@ private:
|
|||||||
QComboBox* m_videoCombo = nullptr;
|
QComboBox* m_videoCombo = nullptr;
|
||||||
QComboBox* m_audioCombo = nullptr;
|
QComboBox* m_audioCombo = nullptr;
|
||||||
QComboBox* m_fpsCombo = nullptr;
|
QComboBox* m_fpsCombo = nullptr;
|
||||||
|
QCheckBox* m_smoothCheck = nullptr;
|
||||||
|
QCheckBox* m_retroCheck = nullptr;
|
||||||
|
QDoubleSpinBox* m_sharpenSpin = nullptr;
|
||||||
|
QDoubleSpinBox* m_brightnessSpin = nullptr;
|
||||||
|
QSpinBox* m_hwContrastSpin = nullptr;
|
||||||
|
QSpinBox* m_hwSaturationSpin = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,18 +9,28 @@
|
|||||||
#include <QMenuBar>
|
#include <QMenuBar>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QSettings>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
MainWindow::MainWindow(const QString& videoDevice,
|
MainWindow::MainWindow(const QString& videoDevice,
|
||||||
const QString& audioDevice,
|
const QString& audioDevice,
|
||||||
int targetFps,
|
int targetFps,
|
||||||
|
bool smoothModeEnabled,
|
||||||
|
bool retroModeEnabled,
|
||||||
|
float brightness,
|
||||||
|
float sharpenAmount,
|
||||||
|
float lastSharpenAmount,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent)
|
||||||
|
, m_lastSharpenAmount(lastSharpenAmount)
|
||||||
, m_videoDevice(videoDevice)
|
, m_videoDevice(videoDevice)
|
||||||
, m_audioDevice(audioDevice)
|
, m_audioDevice(audioDevice)
|
||||||
{
|
{
|
||||||
@@ -28,8 +38,20 @@ MainWindow::MainWindow(const QString& videoDevice,
|
|||||||
resize(1280, 720);
|
resize(1280, 720);
|
||||||
|
|
||||||
m_video = new VideoWidget(this);
|
m_video = new VideoWidget(this);
|
||||||
|
m_video->setSmoothModeEnabled(smoothModeEnabled);
|
||||||
|
m_video->setRetroModeEnabled(retroModeEnabled);
|
||||||
|
m_video->setSharpenAmount(sharpenAmount);
|
||||||
|
m_video->setBrightness(brightness);
|
||||||
setCentralWidget(m_video);
|
setCentralWidget(m_video);
|
||||||
|
|
||||||
|
// Kurzzeitige Einblendung unten links (funktioniert auch im Vollbild,
|
||||||
|
// wo Menü- und Statusleiste ausgeblendet sind).
|
||||||
|
m_overlayLabel = new QLabel(m_video);
|
||||||
|
m_overlayLabel->setStyleSheet(
|
||||||
|
"background-color: rgba(20, 20, 20, 200); color: white;"
|
||||||
|
"padding: 6px 12px; border-radius: 4px; font-size: 13px;");
|
||||||
|
m_overlayLabel->hide();
|
||||||
|
|
||||||
setupMenuBar();
|
setupMenuBar();
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
|
|
||||||
@@ -45,6 +67,12 @@ MainWindow::MainWindow(const QString& videoDevice,
|
|||||||
connect(m_capture, &CaptureThread::captureInfo,
|
connect(m_capture, &CaptureThread::captureInfo,
|
||||||
this, &MainWindow::onCaptureInfo,
|
this, &MainWindow::onCaptureInfo,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
connect(m_capture, &CaptureThread::frameDumped,
|
||||||
|
this, &MainWindow::onFrameDumped,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
connect(m_capture, &CaptureThread::hardwareControlChanged,
|
||||||
|
this, &MainWindow::onHardwareControlChanged,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
m_capture->start();
|
m_capture->start();
|
||||||
|
|
||||||
@@ -65,6 +93,54 @@ MainWindow::~MainWindow()
|
|||||||
|
|
||||||
void MainWindow::setupMenuBar()
|
void MainWindow::setupMenuBar()
|
||||||
{
|
{
|
||||||
|
QMenu* viewMenu = menuBar()->addMenu("&Anzeige");
|
||||||
|
|
||||||
|
m_smoothAction = viewMenu->addAction("Bildglättung\tS");
|
||||||
|
m_smoothAction->setCheckable(true);
|
||||||
|
m_smoothAction->setChecked(m_video->smoothModeEnabled());
|
||||||
|
connect(m_smoothAction, &QAction::triggered, this, &MainWindow::toggleSmoothMode);
|
||||||
|
|
||||||
|
m_retroAction = viewMenu->addAction("Retro-Optik (Scanlines/Maske)\tC");
|
||||||
|
m_retroAction->setCheckable(true);
|
||||||
|
m_retroAction->setChecked(m_video->retroModeEnabled());
|
||||||
|
connect(m_retroAction, &QAction::triggered, this, &MainWindow::toggleRetroMode);
|
||||||
|
|
||||||
|
m_sharpenAction = viewMenu->addAction("Nachschärfen\tX");
|
||||||
|
m_sharpenAction->setCheckable(true);
|
||||||
|
m_sharpenAction->setChecked(m_video->sharpenAmount() > 0.0f);
|
||||||
|
connect(m_sharpenAction, &QAction::triggered, this, &MainWindow::toggleSharpen);
|
||||||
|
|
||||||
|
QAction* sharpenUpAct = viewMenu->addAction("Schärfen +\t+");
|
||||||
|
connect(sharpenUpAct, &QAction::triggered, this, &MainWindow::increaseSharpen);
|
||||||
|
|
||||||
|
QAction* sharpenDownAct = viewMenu->addAction("Schärfen -\t-");
|
||||||
|
connect(sharpenDownAct, &QAction::triggered, this, &MainWindow::decreaseSharpen);
|
||||||
|
|
||||||
|
viewMenu->addSeparator();
|
||||||
|
|
||||||
|
QAction* brighterAct = viewMenu->addAction("Heller\t.");
|
||||||
|
connect(brighterAct, &QAction::triggered, this, &MainWindow::increaseBrightness);
|
||||||
|
|
||||||
|
QAction* darkerAct = viewMenu->addAction("Dunkler\t,");
|
||||||
|
connect(darkerAct, &QAction::triggered, this, &MainWindow::decreaseBrightness);
|
||||||
|
|
||||||
|
viewMenu->addSeparator();
|
||||||
|
|
||||||
|
QAction* rawAct = viewMenu->addAction("Roh-Modus (Diagnose)\tR");
|
||||||
|
rawAct->setCheckable(true);
|
||||||
|
rawAct->setChecked(m_video->rawEnabled());
|
||||||
|
connect(rawAct, &QAction::triggered, this, &MainWindow::toggleRaw);
|
||||||
|
|
||||||
|
QAction* compareAct = viewMenu->addAction("Live-Vergleich (roh/bearbeitet)\tV");
|
||||||
|
compareAct->setCheckable(true);
|
||||||
|
compareAct->setChecked(m_video->compareEnabled());
|
||||||
|
connect(compareAct, &QAction::triggered, this, &MainWindow::toggleCompare);
|
||||||
|
|
||||||
|
viewMenu->addSeparator();
|
||||||
|
|
||||||
|
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
|
||||||
|
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
|
||||||
|
|
||||||
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
|
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
|
||||||
|
|
||||||
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
|
||||||
@@ -72,11 +148,155 @@ void MainWindow::setupMenuBar()
|
|||||||
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
|
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::showOverlayMessage(const QString& text)
|
||||||
|
{
|
||||||
|
if (!m_overlayLabel) return;
|
||||||
|
|
||||||
|
m_overlayLabel->setText(text);
|
||||||
|
m_overlayLabel->adjustSize();
|
||||||
|
|
||||||
|
const int margin = 16;
|
||||||
|
m_overlayLabel->move(margin, m_video->height() - m_overlayLabel->height() - margin);
|
||||||
|
m_overlayLabel->show();
|
||||||
|
m_overlayLabel->raise();
|
||||||
|
|
||||||
|
QTimer::singleShot(1500, m_overlayLabel, &QLabel::hide);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::toggleSmoothMode()
|
||||||
|
{
|
||||||
|
const bool enabled = !m_video->smoothModeEnabled();
|
||||||
|
m_video->setSmoothModeEnabled(enabled);
|
||||||
|
if (m_smoothAction) m_smoothAction->setChecked(enabled);
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue("display/smooth", enabled);
|
||||||
|
|
||||||
|
statusBar()->showMessage(
|
||||||
|
enabled ? "Bildglättung aktiviert" : "Bildglättung deaktiviert", 2000);
|
||||||
|
showOverlayMessage(enabled ? "Bildglättung: an" : "Bildglättung: aus");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::toggleRetroMode()
|
||||||
|
{
|
||||||
|
const bool enabled = !m_video->retroModeEnabled();
|
||||||
|
m_video->setRetroModeEnabled(enabled);
|
||||||
|
if (m_retroAction) m_retroAction->setChecked(enabled);
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue("display/retro", enabled);
|
||||||
|
|
||||||
|
statusBar()->showMessage(
|
||||||
|
enabled ? "Retro-Optik aktiviert" : "Retro-Optik deaktiviert", 2000);
|
||||||
|
showOverlayMessage(enabled ? "Retro-Optik: an" : "Retro-Optik: aus");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::toggleSharpen()
|
||||||
|
{
|
||||||
|
// Quick-Toggle: aus (0) ↔ zuletzt benutzter Wert (nicht der feste
|
||||||
|
// Standardwert!) – merkt sich also z. B. 7.0, nicht immer 2.5.
|
||||||
|
if (m_video->sharpenAmount() > 0.0f) {
|
||||||
|
m_lastSharpenAmount = m_video->sharpenAmount();
|
||||||
|
applySharpen(0.0f);
|
||||||
|
} else {
|
||||||
|
applySharpen(m_lastSharpenAmount > 0.0f ? m_lastSharpenAmount : VideoWidget::kSharpenDefault);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::increaseSharpen()
|
||||||
|
{
|
||||||
|
applySharpen(m_video->sharpenAmount() + VideoWidget::kSharpenStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::decreaseSharpen()
|
||||||
|
{
|
||||||
|
applySharpen(m_video->sharpenAmount() - VideoWidget::kSharpenStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::applySharpen(float value)
|
||||||
|
{
|
||||||
|
value = qBound(VideoWidget::kSharpenMin, value, VideoWidget::kSharpenMax);
|
||||||
|
m_video->setSharpenAmount(value);
|
||||||
|
if (m_sharpenAction) m_sharpenAction->setChecked(value > 0.0f);
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue("display/sharpen", static_cast<double>(value));
|
||||||
|
if (value > 0.0f) {
|
||||||
|
m_lastSharpenAmount = value;
|
||||||
|
settings.setValue("display/sharpen_last", static_cast<double>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
statusBar()->showMessage(QString("Nachschärfen: %1").arg(value, 0, 'f', 1), 2000);
|
||||||
|
showOverlayMessage(QString("Nachschärfen: %1").arg(value, 0, 'f', 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::increaseBrightness()
|
||||||
|
{
|
||||||
|
applyBrightness(m_video->brightness() + VideoWidget::kBrightnessStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::decreaseBrightness()
|
||||||
|
{
|
||||||
|
applyBrightness(m_video->brightness() - VideoWidget::kBrightnessStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::toggleRaw()
|
||||||
|
{
|
||||||
|
const bool enabled = !m_video->rawEnabled();
|
||||||
|
m_video->setRawEnabled(enabled);
|
||||||
|
|
||||||
|
const QString msg = enabled ? "Roh-Modus: an (alle Effekte umgangen)" : "Roh-Modus: aus";
|
||||||
|
statusBar()->showMessage(msg, 2000);
|
||||||
|
showOverlayMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::toggleCompare()
|
||||||
|
{
|
||||||
|
const bool enabled = !m_video->compareEnabled();
|
||||||
|
m_video->setCompareEnabled(enabled);
|
||||||
|
|
||||||
|
const QString msg = enabled ? "Vergleich: an (links roh, rechts bearbeitet)" : "Vergleich: aus";
|
||||||
|
statusBar()->showMessage(msg, 2000);
|
||||||
|
showOverlayMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::saveScreenshot()
|
||||||
|
{
|
||||||
|
const QImage img = m_video->grabProcessedFrame();
|
||||||
|
if (img.isNull()) {
|
||||||
|
statusBar()->showMessage("Screenshot fehlgeschlagen (kein Bild)", 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString path = QDir::homePath() + "/hdmiviewer_screenshot_" +
|
||||||
|
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss") + ".png";
|
||||||
|
|
||||||
|
if (img.save(path, "PNG")) {
|
||||||
|
statusBar()->showMessage("Screenshot gespeichert: " + path, 5000);
|
||||||
|
showOverlayMessage("Screenshot gespeichert");
|
||||||
|
} else {
|
||||||
|
statusBar()->showMessage("Screenshot konnte nicht gespeichert werden: " + path, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::applyBrightness(float value)
|
||||||
|
{
|
||||||
|
value = qBound(VideoWidget::kBrightnessMin, value, VideoWidget::kBrightnessMax);
|
||||||
|
m_video->setBrightness(value);
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue("display/brightness", static_cast<double>(value));
|
||||||
|
|
||||||
|
statusBar()->showMessage(
|
||||||
|
QString("Helligkeit: %1 %").arg(qRound(value * 100.0f)), 2000);
|
||||||
|
showOverlayMessage(QString("Helligkeit: %1 %").arg(qRound(value * 100.0f)));
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::showAbout()
|
void MainWindow::showAbout()
|
||||||
{
|
{
|
||||||
auto* dlg = new QDialog(this);
|
auto* dlg = new QDialog(this);
|
||||||
dlg->setWindowTitle("Über HDMI Viewer");
|
dlg->setWindowTitle("Über HDMI Viewer");
|
||||||
dlg->setFixedSize(420, 340);
|
dlg->setFixedSize(460, 470);
|
||||||
dlg->setModal(true);
|
dlg->setModal(true);
|
||||||
|
|
||||||
auto* root = new QVBoxLayout(dlg);
|
auto* root = new QVBoxLayout(dlg);
|
||||||
@@ -125,26 +345,37 @@ void MainWindow::showAbout()
|
|||||||
grid->addWidget(v, row, 1);
|
grid->addWidget(v, row, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
addRow(0, "Version", "1.3.0");
|
addRow(0, "Version", "1.4.0");
|
||||||
addRow(1, "Entwickler", "Dany Thinnes");
|
addRow(1, "Entwickler", "Dany Thinnes");
|
||||||
addRow(2, "Projekt", "Projekt Hirnfrei");
|
addRow(2, "Projekt", "Projekt Hirnfrei");
|
||||||
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
|
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
|
||||||
addRow(4, "Sprache", "C++17");
|
addRow(4, "Sprache", "C++17");
|
||||||
addRow(5, "Video-API", "V4L2 · MJPEG / YUYV / NV12");
|
addRow(5, "Video-API", "V4L2 · MJPEG / YUYV / NV12");
|
||||||
addRow(6, "Audio-API", "ALSA");
|
addRow(6, "Audio-API", "ALSA");
|
||||||
|
addRow(7, "Rendering", "OpenGL 3.3 · Bildglättung / Retro-Optik");
|
||||||
|
|
||||||
// ── Tastenkürzel-Hinweis ──────────────────────────────────────────────────
|
// ── Tastenkürzel-Hinweis ──────────────────────────────────────────────────
|
||||||
auto* sep2 = new QFrame(body);
|
auto* sep2 = new QFrame(body);
|
||||||
sep2->setFrameShape(QFrame::HLine);
|
sep2->setFrameShape(QFrame::HLine);
|
||||||
sep2->setStyleSheet("color: #313244;");
|
sep2->setStyleSheet("color: #313244;");
|
||||||
grid->addWidget(sep2, 7, 0, 1, 2);
|
grid->addWidget(sep2, 8, 0, 1, 2);
|
||||||
|
|
||||||
auto* keysLabel = new QLabel(
|
auto* keysLabel = new QLabel(
|
||||||
"<small><b>F</b> Vollbild ein/aus "
|
"<small><b>F</b> Vollbild "
|
||||||
"<b>Esc</b> Zurück zu Einstellungen</small>", body);
|
"<b>S</b> Glättung "
|
||||||
|
"<b>C</b> Retro "
|
||||||
|
"<b>X</b>/<b>+</b>/<b>-</b> Schärfen "
|
||||||
|
"<b>,</b>/<b>.</b> Helligkeit<br>"
|
||||||
|
"<b>1</b>/<b>2</b> HW-Kontrast "
|
||||||
|
"<b>3</b>/<b>4</b> HW-Sättigung "
|
||||||
|
"<b>R</b> Roh-Diagnose<br>"
|
||||||
|
"<b>V</b> Vergleich "
|
||||||
|
"<b>P</b> Screenshot "
|
||||||
|
"<b>D</b> Frame dumpen "
|
||||||
|
"<b>Esc</b> Zurück</small>", body);
|
||||||
keysLabel->setStyleSheet("color: #6c7086;");
|
keysLabel->setStyleSheet("color: #6c7086;");
|
||||||
keysLabel->setAlignment(Qt::AlignCenter);
|
keysLabel->setAlignment(Qt::AlignCenter);
|
||||||
grid->addWidget(keysLabel, 8, 0, 1, 2);
|
grid->addWidget(keysLabel, 9, 0, 1, 2);
|
||||||
|
|
||||||
// ── Schließen-Button ──────────────────────────────────────────────────────
|
// ── Schließen-Button ──────────────────────────────────────────────────────
|
||||||
auto* btnRow = new QHBoxLayout;
|
auto* btnRow = new QHBoxLayout;
|
||||||
@@ -194,6 +425,52 @@ void MainWindow::keyPressEvent(QKeyEvent* event)
|
|||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Qt::Key_S:
|
||||||
|
toggleSmoothMode();
|
||||||
|
break;
|
||||||
|
case Qt::Key_C:
|
||||||
|
toggleRetroMode();
|
||||||
|
break;
|
||||||
|
case Qt::Key_X:
|
||||||
|
toggleSharpen();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Plus:
|
||||||
|
case Qt::Key_Equal: // '+' liegt auf vielen Layouts ohne Shift auf dieser Taste
|
||||||
|
increaseSharpen();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Minus:
|
||||||
|
decreaseSharpen();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Period:
|
||||||
|
increaseBrightness();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Comma:
|
||||||
|
decreaseBrightness();
|
||||||
|
break;
|
||||||
|
case Qt::Key_R:
|
||||||
|
toggleRaw();
|
||||||
|
break;
|
||||||
|
case Qt::Key_D:
|
||||||
|
if (m_capture) m_capture->requestFrameDump();
|
||||||
|
break;
|
||||||
|
case Qt::Key_V:
|
||||||
|
toggleCompare();
|
||||||
|
break;
|
||||||
|
case Qt::Key_P:
|
||||||
|
saveScreenshot();
|
||||||
|
break;
|
||||||
|
case Qt::Key_1:
|
||||||
|
if (m_capture) m_capture->adjustHardwareContrast(-5);
|
||||||
|
break;
|
||||||
|
case Qt::Key_2:
|
||||||
|
if (m_capture) m_capture->adjustHardwareContrast(5);
|
||||||
|
break;
|
||||||
|
case Qt::Key_3:
|
||||||
|
if (m_capture) m_capture->adjustHardwareSaturation(-5);
|
||||||
|
break;
|
||||||
|
case Qt::Key_4:
|
||||||
|
if (m_capture) m_capture->adjustHardwareSaturation(5);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
QMainWindow::keyPressEvent(event);
|
QMainWindow::keyPressEvent(event);
|
||||||
}
|
}
|
||||||
@@ -226,7 +503,7 @@ void MainWindow::updateStatusBar()
|
|||||||
info = QString("%1×%2 @ %3fps %4 ")
|
info = QString("%1×%2 @ %3fps %4 ")
|
||||||
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
|
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
|
||||||
|
|
||||||
info += QString("Video: %1 Audio: %2 [F] Vollbild [Esc] Einstellungen")
|
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
|
||||||
.arg(m_videoDevice,
|
.arg(m_videoDevice,
|
||||||
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);
|
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);
|
||||||
|
|
||||||
@@ -244,6 +521,20 @@ void MainWindow::onCaptureInfo(int width, int height, int fps, const QString& fo
|
|||||||
.arg(width).arg(height).arg(fps).arg(format));
|
.arg(width).arg(height).arg(fps).arg(format));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::onFrameDumped(const QString& path)
|
||||||
|
{
|
||||||
|
const QString msg = "Frame gespeichert: " + path;
|
||||||
|
statusBar()->showMessage(msg, 5000);
|
||||||
|
showOverlayMessage("Frame gespeichert (siehe Home-Ordner)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onHardwareControlChanged(const QString& label, int value, int minVal, int maxVal)
|
||||||
|
{
|
||||||
|
const QString msg = QString("%1: %2 (%3..%4)").arg(label).arg(value).arg(minVal).arg(maxVal);
|
||||||
|
statusBar()->showMessage(msg, 2000);
|
||||||
|
showOverlayMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::onError(const QString& msg)
|
void MainWindow::onError(const QString& msg)
|
||||||
{
|
{
|
||||||
statusBar()->showMessage("⚠ " + msg, 8000);
|
statusBar()->showMessage("⚠ " + msg, 8000);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
class VideoWidget;
|
class VideoWidget;
|
||||||
class CaptureThread;
|
class CaptureThread;
|
||||||
class AudioThread;
|
class AudioThread;
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QAction;
|
||||||
|
class QLabel;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
@@ -15,6 +19,11 @@ public:
|
|||||||
MainWindow(const QString& videoDevice,
|
MainWindow(const QString& videoDevice,
|
||||||
const QString& audioDevice,
|
const QString& audioDevice,
|
||||||
int targetFps,
|
int targetFps,
|
||||||
|
bool smoothModeEnabled = true,
|
||||||
|
bool retroModeEnabled = false,
|
||||||
|
float brightness = 1.0f,
|
||||||
|
float sharpenAmount = 0.0f,
|
||||||
|
float lastSharpenAmount = 2.5f,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~MainWindow() override;
|
~MainWindow() override;
|
||||||
|
|
||||||
@@ -27,17 +36,37 @@ protected:
|
|||||||
private slots:
|
private slots:
|
||||||
void onError(const QString& msg);
|
void onError(const QString& msg);
|
||||||
void onCaptureInfo(int width, int height, int fps, const QString& format);
|
void onCaptureInfo(int width, int height, int fps, const QString& format);
|
||||||
|
void onFrameDumped(const QString& path);
|
||||||
|
void onHardwareControlChanged(const QString& label, int value, int minVal, int maxVal);
|
||||||
void showAbout();
|
void showAbout();
|
||||||
|
void toggleSmoothMode();
|
||||||
|
void toggleRetroMode();
|
||||||
|
void toggleSharpen();
|
||||||
|
void increaseSharpen();
|
||||||
|
void decreaseSharpen();
|
||||||
|
void increaseBrightness();
|
||||||
|
void decreaseBrightness();
|
||||||
|
void toggleRaw();
|
||||||
|
void toggleCompare();
|
||||||
|
void saveScreenshot();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupMenuBar();
|
void setupMenuBar();
|
||||||
void toggleFullscreen();
|
void toggleFullscreen();
|
||||||
void stopAll();
|
void stopAll();
|
||||||
void updateStatusBar();
|
void updateStatusBar();
|
||||||
|
void applyBrightness(float value);
|
||||||
|
void applySharpen(float value);
|
||||||
|
void showOverlayMessage(const QString& text);
|
||||||
|
|
||||||
VideoWidget* m_video = nullptr;
|
VideoWidget* m_video = nullptr;
|
||||||
CaptureThread* m_capture = nullptr;
|
CaptureThread* m_capture = nullptr;
|
||||||
AudioThread* m_audio = nullptr;
|
AudioThread* m_audio = nullptr;
|
||||||
|
QAction* m_smoothAction = nullptr;
|
||||||
|
QAction* m_retroAction = nullptr;
|
||||||
|
QAction* m_sharpenAction = nullptr;
|
||||||
|
QLabel* m_overlayLabel = nullptr;
|
||||||
|
float m_lastSharpenAmount = 2.5f; // für Taste X: Wert vorm letzten Ausschalten
|
||||||
|
|
||||||
QString m_videoDevice;
|
QString m_videoDevice;
|
||||||
QString m_audioDevice;
|
QString m_audioDevice;
|
||||||
|
|||||||
@@ -1,45 +1,450 @@
|
|||||||
#include "VideoWidget.h"
|
#include "VideoWidget.h"
|
||||||
|
|
||||||
|
#include <QOpenGLShaderProgram>
|
||||||
|
#include <QOpenGLTexture>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPaintEvent>
|
#include <QVector2D>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Unit-Quad in NDC, per Vertex-Shader auf die gewünschte Letterbox-Größe
|
||||||
|
// skaliert. Texturkoordinaten so gewählt, dass QImage-Zeile 0 (oben) auf
|
||||||
|
// v=0 (oben im Bild) landet.
|
||||||
|
const float kQuadVerts[] = {
|
||||||
|
// x, y, u, v
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f,
|
||||||
|
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, 1.0f, 1.0f, 0.0f,
|
||||||
|
1.0f, -1.0f, 1.0f, 1.0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* kVertexShaderSrc = R"GLSL(
|
||||||
|
#version 330 core
|
||||||
|
layout(location = 0) in vec2 a_pos;
|
||||||
|
layout(location = 1) in vec2 a_tex;
|
||||||
|
|
||||||
|
uniform vec2 u_scale;
|
||||||
|
uniform vec2 u_offset;
|
||||||
|
|
||||||
|
out vec2 v_tex;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 pos = a_pos * u_scale + u_offset;
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
v_tex = a_tex;
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
// Zwei unabhängige, kombinierbare Effekte:
|
||||||
|
//
|
||||||
|
// 1) Bildglättung (u_smooth) – die eigentliche Qualitätsverbesserung: trennt
|
||||||
|
// Luma/Chroma (wie ein Composite-Signal es tut) und behandelt sie
|
||||||
|
// unterschiedlich. Chroma wird stärker weichgezeichnet, weil billige
|
||||||
|
// Composite→HDMI-Chips dort Dot-Crawl/Farbrauschen produzieren – das Auge
|
||||||
|
// ist für Farbdetails ohnehin unempfindlicher. Luma wird nur leicht
|
||||||
|
// geglättet (nimmt harte, blockige Scaler-Kanten raus) und danach dezent
|
||||||
|
// gegengeschärft, damit das Bild nicht matschig wirkt. Kein Nostalgie-
|
||||||
|
// Effekt, sondern Rekonstruktion in Richtung dessen, was ein Röhrenbild
|
||||||
|
// ohnehin automatisch getan hätte.
|
||||||
|
//
|
||||||
|
// 2) Retro-Optik (u_retro) – der Nostalgie-Look von vorhin: Scanlines,
|
||||||
|
// Aperture-Grille-Maske, leichte Wölbung, Vignette. Rein optisch, wird
|
||||||
|
// nach der Glättung obendrauf gelegt, falls gewünscht.
|
||||||
|
const char* kFragmentShaderSrc = R"GLSL(
|
||||||
|
#version 330 core
|
||||||
|
in vec2 v_tex;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
uniform sampler2D u_tex;
|
||||||
|
uniform vec2 u_texSize;
|
||||||
|
uniform int u_smooth;
|
||||||
|
uniform int u_retro;
|
||||||
|
uniform float u_sharpen;
|
||||||
|
uniform float u_gamma;
|
||||||
|
uniform int u_raw;
|
||||||
|
uniform int u_compare;
|
||||||
|
|
||||||
|
vec3 rgb2ycbcr(vec3 c)
|
||||||
|
{
|
||||||
|
float y = dot(c, vec3(0.299, 0.587, 0.114));
|
||||||
|
float cb = dot(c, vec3(-0.168736, -0.331264, 0.5)) + 0.5;
|
||||||
|
float cr = dot(c, vec3(0.5, -0.418688, -0.081312)) + 0.5;
|
||||||
|
return vec3(y, cb, cr);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 ycbcr2rgb(vec3 c)
|
||||||
|
{
|
||||||
|
float y = c.x;
|
||||||
|
float cb = c.y - 0.5;
|
||||||
|
float cr = c.z - 0.5;
|
||||||
|
return vec3(
|
||||||
|
y + 1.402 * cr,
|
||||||
|
y - 0.344136 * cb - 0.714136 * cr,
|
||||||
|
y + 1.772 * cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rekonstruiert Luma und Chroma getrennt mit einem echten, breiten 2D-
|
||||||
|
// Tiefpass (Radius 4 Texel statt vorher 1-2). Bei 1920×1080 ist ein
|
||||||
|
// 1-2-Texel-Radius schlicht zu schmal, um überhaupt wahrnehmbar zu sein –
|
||||||
|
// und JPEG-Blockartefakte sitzen im 8×8-Pixel-Raster, das ein so schmaler
|
||||||
|
// Filter gar nicht berührt. Radius 4 ist die minimal sinnvolle Größe.
|
||||||
|
vec3 reconstruct(vec2 uv)
|
||||||
|
{
|
||||||
|
vec2 tx = vec2(1.0 / max(u_texSize.x, 1.0), 0.0);
|
||||||
|
vec2 ty = vec2(0.0, 1.0 / max(u_texSize.y, 1.0));
|
||||||
|
|
||||||
|
vec3 center = rgb2ycbcr(texture(u_tex, uv).rgb);
|
||||||
|
|
||||||
|
// Symmetrischer 9-Tap-Kern (Radius 4), auf 1.0 normiert
|
||||||
|
float w0 = 0.20, w1 = 0.16, w2 = 0.12, w3 = 0.08, w4 = 0.04;
|
||||||
|
|
||||||
|
vec3 blurH = center * w0;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv - 1.0*tx).rgb) * w1;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv + 1.0*tx).rgb) * w1;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv - 2.0*tx).rgb) * w2;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv + 2.0*tx).rgb) * w2;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv - 3.0*tx).rgb) * w3;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv + 3.0*tx).rgb) * w3;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv - 4.0*tx).rgb) * w4;
|
||||||
|
blurH += rgb2ycbcr(texture(u_tex, uv + 4.0*tx).rgb) * w4;
|
||||||
|
|
||||||
|
vec3 blurV = center * w0;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv - 1.0*ty).rgb) * w1;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv + 1.0*ty).rgb) * w1;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv - 2.0*ty).rgb) * w2;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv + 2.0*ty).rgb) * w2;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv - 3.0*ty).rgb) * w3;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv + 3.0*ty).rgb) * w3;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv - 4.0*ty).rgb) * w4;
|
||||||
|
blurV += rgb2ycbcr(texture(u_tex, uv + 4.0*ty).rgb) * w4;
|
||||||
|
|
||||||
|
vec3 blur2D = (blurH + blurV) * 0.5;
|
||||||
|
|
||||||
|
// Adaptive Stärke: dunkle Bereiche stärker (Kompressionsrauschen), aber
|
||||||
|
// die Basis liegt jetzt nahe 100% reinem Blur – KEIN interner
|
||||||
|
// Gegenschärfen-Anteil mehr (der hat vorher einen Großteil des Effekts
|
||||||
|
// wieder zurückgenommen, ohne dass es nach außen sichtbar war). Schärfen
|
||||||
|
// ist jetzt ausschließlich Aufgabe von sharpen()/Taste X.
|
||||||
|
float darkFactor = clamp(1.0 - center.x / 0.3, 0.0, 1.0);
|
||||||
|
float lumaMixAmt = mix(0.92, 0.99, darkFactor);
|
||||||
|
float chromaMixAmt = mix(0.95, 0.99, darkFactor);
|
||||||
|
|
||||||
|
float lumaOut = mix(center.x, blur2D.x, lumaMixAmt);
|
||||||
|
vec2 chromaOut = mix(center.yz, blur2D.yz, chromaMixAmt);
|
||||||
|
|
||||||
|
return ycbcr2rgb(vec3(lumaOut, chromaOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nachschärfen (Unsharp Mask) übers gesamte Bild, unabhängig von Glättung/
|
||||||
|
// Retro-Optik. Stärke kommt jetzt vollständig aus u_sharpen (per Taste +/-
|
||||||
|
// live einstellbar), damit sich die Wirkschwelle experimentell finden lässt.
|
||||||
|
vec3 sharpen(vec2 uv, vec3 baseColor, float amount)
|
||||||
|
{
|
||||||
|
vec2 tx = vec2(1.0 / max(u_texSize.x, 1.0), 0.0);
|
||||||
|
vec2 ty = vec2(0.0, 1.0 / max(u_texSize.y, 1.0));
|
||||||
|
|
||||||
|
vec3 raw = texture(u_tex, uv).rgb;
|
||||||
|
vec3 blur = raw * 0.20;
|
||||||
|
blur += (texture(u_tex, uv - tx).rgb + texture(u_tex, uv + tx).rgb
|
||||||
|
+ texture(u_tex, uv - ty).rgb + texture(u_tex, uv + ty).rgb) * 0.10;
|
||||||
|
blur += (texture(u_tex, uv - 2.0*tx).rgb + texture(u_tex, uv + 2.0*tx).rgb
|
||||||
|
+ texture(u_tex, uv - 2.0*ty).rgb + texture(u_tex, uv + 2.0*ty).rgb) * 0.10;
|
||||||
|
|
||||||
|
return baseColor + amount * (raw - blur);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 uv = v_tex;
|
||||||
|
|
||||||
|
// Diagnose: komplette Umgehung aller Verarbeitung, 1:1 wie im Speicher.
|
||||||
|
if (u_raw == 1) {
|
||||||
|
fragColor = vec4(texture(u_tex, uv).rgb, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float r2 = 0.0;
|
||||||
|
|
||||||
|
if (u_retro == 1) {
|
||||||
|
vec2 c = uv * 2.0 - 1.0;
|
||||||
|
r2 = dot(c, c);
|
||||||
|
vec2 warped = c * (1.0 + 0.035 * r2);
|
||||||
|
uv = warped * 0.5 + 0.5;
|
||||||
|
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
|
||||||
|
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 color = (u_smooth == 1) ? reconstruct(uv) : texture(u_tex, uv).rgb;
|
||||||
|
|
||||||
|
if (u_retro == 1) {
|
||||||
|
float lines = 240.0;
|
||||||
|
float linePhase = fract(uv.y * lines) * 2.0 - 1.0;
|
||||||
|
float scan = 1.0 - 0.28 * (1.0 - linePhase * linePhase);
|
||||||
|
color *= scan;
|
||||||
|
|
||||||
|
float maskX = mod(gl_FragCoord.x, 3.0);
|
||||||
|
vec3 mask = vec3(0.82);
|
||||||
|
if (maskX < 1.0) mask.r = 1.08;
|
||||||
|
else if (maskX < 2.0) mask.g = 1.08;
|
||||||
|
else mask.b = 1.08;
|
||||||
|
color *= mask;
|
||||||
|
|
||||||
|
float vig = clamp(1.0 - 0.22 * r2, 0.0, 1.0);
|
||||||
|
color *= vig;
|
||||||
|
color *= 1.18;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signalbereich-Korrektur: viele billige Grabber/Scaler liefern effektiv
|
||||||
|
// nur Studio-/Broadcast-Range statt vollem 0–255-Kontrastumfang –
|
||||||
|
// unabhängig vom Pixelformat, betrifft also auch MJPEG (wo eine reine
|
||||||
|
// YUV-seitige Korrektur nicht greifen würde). Schwarzpunkt bewusst nahe
|
||||||
|
// 0, damit möglichst viel Schattendetail erhalten bleibt – das adaptive
|
||||||
|
// Rauschunterdrücken oben in reconstruct() fängt das dabei entstehende
|
||||||
|
// Rauschen in genau diesen dunklen Bereichen gezielt ab.
|
||||||
|
const float kBlack = 1.0 / 255.0;
|
||||||
|
const float kWhite = 235.0 / 255.0;
|
||||||
|
color = clamp((color - kBlack) / (kWhite - kBlack), 0.0, 1.0);
|
||||||
|
|
||||||
|
if (u_sharpen > 0.0) {
|
||||||
|
color = sharpen(uv, color, u_sharpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helligkeit: Gamma-Anpassung NUR auf Luma (nicht pro RGB-Kanal einzeln!)
|
||||||
|
// – sonst verschiebt sich bei verrauschten/dunklen Pixeln der Farbton,
|
||||||
|
// und Kompressionsrauschen wird bunt statt nur heller.
|
||||||
|
vec3 ycc = rgb2ycbcr(color);
|
||||||
|
ycc.x = pow(max(ycc.x, 0.0), 1.0 / u_gamma);
|
||||||
|
|
||||||
|
// Kontrast-Punch: leichte S-Kurve um die Mitte, deutlich zurückhaltender
|
||||||
|
// als vorher (war 1.12) – sattere Schwarz-/Hellwerte, ohne bei dunklen
|
||||||
|
// Szenen spürbar Helligkeit zu kosten.
|
||||||
|
ycc.x = clamp((ycc.x - 0.5) * 1.05 + 0.5, 0.0, 1.0);
|
||||||
|
float targetY = ycc.x;
|
||||||
|
|
||||||
|
// Sättigung: etwas kräftiger als roh, aber ebenfalls zurückhaltender
|
||||||
|
// (war 1.35). Wichtiger: nach der Sättigungsanhebung wird die
|
||||||
|
// tatsächliche Helligkeit der resultierenden Farbe gemessen und wieder
|
||||||
|
// auf targetY zurückskaliert. Grund: Die YCbCr→RGB-Formel zieht den
|
||||||
|
// Grün-Kanal bei der Rückrechnung von Cb/Cr ab – bei warmen/orangenen
|
||||||
|
// Szenen (starkes Cr) senkt eine Sättigungsanhebung dadurch spürbar die
|
||||||
|
// wahrgenommene Helligkeit, obwohl Y unverändert blieb. Diese
|
||||||
|
// Renormierung verhindert genau dieses szenenabhängige Abdunkeln.
|
||||||
|
ycc.y = clamp((ycc.y - 0.5) * 1.15 + 0.5, 0.0, 1.0);
|
||||||
|
ycc.z = clamp((ycc.z - 0.5) * 1.15 + 0.5, 0.0, 1.0);
|
||||||
|
|
||||||
|
color = ycbcr2rgb(ycc);
|
||||||
|
float actualY = dot(color, vec3(0.299, 0.587, 0.114));
|
||||||
|
if (actualY > 0.001) {
|
||||||
|
color *= targetY / actualY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live-Vergleich: linke Bildhälfte roh, rechte Hälfte komplett bearbeitet
|
||||||
|
// (mit allen aktuell aktiven Reglern) – zum direkten Abgleich beim
|
||||||
|
// Fein-Tunen. Nutzt bewusst v_tex (unverzerrte Original-Koordinate),
|
||||||
|
// nicht das ggf. durch Retro-Optik gewölbte uv, damit die rohe Seite
|
||||||
|
// wirklich unverändert bleibt.
|
||||||
|
if (u_compare == 1) {
|
||||||
|
vec3 rawColor = texture(u_tex, v_tex).rgb;
|
||||||
|
if (v_tex.x < 0.5) {
|
||||||
|
color = rawColor;
|
||||||
|
}
|
||||||
|
if (abs(v_tex.x - 0.5) < 0.0015) {
|
||||||
|
color = vec3(1.0, 0.9, 0.1); // gelbe Trennlinie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
VideoWidget::VideoWidget(QWidget* parent)
|
VideoWidget::VideoWidget(QWidget* parent)
|
||||||
: QWidget(parent)
|
: QOpenGLWidget(parent)
|
||||||
{
|
{
|
||||||
setMinimumSize(640, 360);
|
setMinimumSize(640, 360);
|
||||||
// Schwarzer Hintergrund
|
}
|
||||||
QPalette pal = palette();
|
|
||||||
pal.setColor(QPalette::Window, Qt::black);
|
VideoWidget::~VideoWidget()
|
||||||
setPalette(pal);
|
{
|
||||||
setAutoFillBackground(true);
|
makeCurrent();
|
||||||
|
delete m_texture;
|
||||||
|
m_texture = nullptr;
|
||||||
|
if (m_vbo) { glDeleteBuffers(1, &m_vbo); m_vbo = 0; }
|
||||||
|
if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; }
|
||||||
|
delete m_program;
|
||||||
|
m_program = nullptr;
|
||||||
|
doneCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoWidget::setFrame(const QImage& image)
|
void VideoWidget::setFrame(const QImage& image)
|
||||||
{
|
{
|
||||||
if (image.isNull()) return;
|
if (image.isNull()) return;
|
||||||
m_pixmap = QPixmap::fromImage(image);
|
m_pendingImage = image;
|
||||||
|
m_pendingDirty = true;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoWidget::paintEvent(QPaintEvent* /*event*/)
|
void VideoWidget::setSmoothModeEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_smoothEnabled == enabled) return;
|
||||||
|
m_smoothEnabled = enabled;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::setRetroModeEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_retroEnabled == enabled) return;
|
||||||
|
m_retroEnabled = enabled;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::setSharpenAmount(float amount)
|
||||||
|
{
|
||||||
|
amount = qBound(kSharpenMin, amount, kSharpenMax);
|
||||||
|
if (qFuzzyCompare(m_sharpenAmount, amount)) return;
|
||||||
|
m_sharpenAmount = amount;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::setRawEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_rawEnabled == enabled) return;
|
||||||
|
m_rawEnabled = enabled;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::setCompareEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (m_compareEnabled == enabled) return;
|
||||||
|
m_compareEnabled = enabled;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::setBrightness(float gamma)
|
||||||
|
{
|
||||||
|
gamma = qBound(kBrightnessMin, gamma, kBrightnessMax);
|
||||||
|
if (qFuzzyCompare(m_brightness, gamma)) return;
|
||||||
|
m_brightness = gamma;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::initializeGL()
|
||||||
|
{
|
||||||
|
initializeOpenGLFunctions();
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
m_program = new QOpenGLShaderProgram(this);
|
||||||
|
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShaderSrc);
|
||||||
|
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShaderSrc);
|
||||||
|
m_program->link();
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVerts), kQuadVerts, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
|
||||||
|
reinterpret_cast<void*>(2 * sizeof(float)));
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::resizeGL(int w, int h)
|
||||||
|
{
|
||||||
|
glViewport(0, 0, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::uploadPendingFrame()
|
||||||
|
{
|
||||||
|
if (!m_pendingDirty || m_pendingImage.isNull())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QImage rgba = m_pendingImage.convertToFormat(QImage::Format_RGBA8888);
|
||||||
|
|
||||||
|
if (!m_texture || m_texture->width() != rgba.width() || m_texture->height() != rgba.height()) {
|
||||||
|
delete m_texture;
|
||||||
|
m_texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
|
||||||
|
m_texture->create();
|
||||||
|
m_texture->setFormat(QOpenGLTexture::RGBA8_UNorm);
|
||||||
|
m_texture->setSize(rgba.width(), rgba.height());
|
||||||
|
m_texture->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8);
|
||||||
|
m_texture->setMinificationFilter(QOpenGLTexture::Linear);
|
||||||
|
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||||
|
m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_texture->setData(QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, rgba.constBits());
|
||||||
|
|
||||||
|
m_imageSize = rgba.size();
|
||||||
|
m_hasFrame = true;
|
||||||
|
m_pendingDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::drawNoSignal()
|
||||||
{
|
{
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
p.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
||||||
|
|
||||||
if (m_pixmap.isNull()) {
|
|
||||||
p.fillRect(rect(), Qt::black);
|
|
||||||
p.setPen(Qt::darkGray);
|
p.setPen(Qt::darkGray);
|
||||||
p.drawText(rect(), Qt::AlignCenter, "Kein Signal");
|
p.drawText(rect(), Qt::AlignCenter, "Kein Signal");
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoWidget::paintGL()
|
||||||
|
{
|
||||||
|
uploadPendingFrame();
|
||||||
|
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
if (!m_hasFrame || !m_texture) {
|
||||||
|
drawNoSignal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seitenverhältnis bewahren (letterbox/pillarbox)
|
const float viewW = static_cast<float>(width());
|
||||||
QRect target = rect();
|
const float viewH = static_cast<float>(height());
|
||||||
QSize scaled = m_pixmap.size().scaled(target.size(), Qt::KeepAspectRatio);
|
const float imgW = static_cast<float>(m_imageSize.width());
|
||||||
QPoint topLeft(
|
const float imgH = static_cast<float>(m_imageSize.height());
|
||||||
target.x() + (target.width() - scaled.width()) / 2,
|
|
||||||
target.y() + (target.height() - scaled.height()) / 2
|
float scaleX = 1.0f;
|
||||||
);
|
float scaleY = 1.0f;
|
||||||
p.fillRect(rect(), Qt::black);
|
if (imgW > 0.0f && imgH > 0.0f && viewW > 0.0f && viewH > 0.0f) {
|
||||||
p.drawPixmap(QRect(topLeft, scaled), m_pixmap, m_pixmap.rect());
|
const float imageAspect = imgW / imgH;
|
||||||
|
const float viewAspect = viewW / viewH;
|
||||||
|
if (imageAspect > viewAspect) {
|
||||||
|
scaleX = 1.0f;
|
||||||
|
scaleY = viewAspect / imageAspect;
|
||||||
|
} else {
|
||||||
|
scaleY = 1.0f;
|
||||||
|
scaleX = imageAspect / viewAspect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_program->bind();
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
m_texture->bind(0);
|
||||||
|
|
||||||
|
m_program->setUniformValue("u_scale", QVector2D(scaleX, scaleY));
|
||||||
|
m_program->setUniformValue("u_offset", QVector2D(0.0f, 0.0f));
|
||||||
|
m_program->setUniformValue("u_tex", 0);
|
||||||
|
m_program->setUniformValue("u_texSize", QVector2D(imgW, imgH));
|
||||||
|
m_program->setUniformValue("u_smooth", m_smoothEnabled ? 1 : 0);
|
||||||
|
m_program->setUniformValue("u_retro", m_retroEnabled ? 1 : 0);
|
||||||
|
m_program->setUniformValue("u_sharpen", m_sharpenAmount);
|
||||||
|
m_program->setUniformValue("u_gamma", m_brightness);
|
||||||
|
m_program->setUniformValue("u_raw", m_rawEnabled ? 1 : 0);
|
||||||
|
m_program->setUniformValue("u_compare", m_compareEnabled ? 1 : 0);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
m_texture->release();
|
||||||
|
glBindVertexArray(0);
|
||||||
|
m_program->release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,79 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QOpenGLWidget>
|
||||||
|
#include <QOpenGLExtraFunctions>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QPixmap>
|
#include <QSize>
|
||||||
|
|
||||||
class VideoWidget : public QWidget
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QOpenGLShaderProgram;
|
||||||
|
class QOpenGLTexture;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
// VideoWidget zeichnet das eingehende Frame per OpenGL (statt QPainter/CPU).
|
||||||
|
// Zwei unabhängige Effekte können live per Shader zugeschaltet werden:
|
||||||
|
// Bildglättung (Chroma/Luma-Rekonstruktion gegen Scaler-Artefakte, echte
|
||||||
|
// Bildverbesserung) und Retro-Optik (Scanlines/Maske/Wölbung, Nostalgie-Look).
|
||||||
|
// QOpenGLExtraFunctions (statt nur QOpenGLFunctions) wird benötigt, weil
|
||||||
|
// Vertex-Array-Objects (glGenVertexArrays & Co.) nicht Teil des reinen
|
||||||
|
// ES2-kompatiblen QOpenGLFunctions-Subsets sind.
|
||||||
|
class VideoWidget : public QOpenGLWidget, protected QOpenGLExtraFunctions
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit VideoWidget(QWidget* parent = nullptr);
|
explicit VideoWidget(QWidget* parent = nullptr);
|
||||||
|
~VideoWidget() override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setFrame(const QImage& image);
|
void setFrame(const QImage& image);
|
||||||
|
void setSmoothModeEnabled(bool enabled);
|
||||||
|
void setRetroModeEnabled(bool enabled);
|
||||||
|
void setSharpenAmount(float amount);
|
||||||
|
void setBrightness(float gamma);
|
||||||
|
void setRawEnabled(bool enabled);
|
||||||
|
void setCompareEnabled(bool enabled);
|
||||||
|
bool smoothModeEnabled() const { return m_smoothEnabled; }
|
||||||
|
bool retroModeEnabled() const { return m_retroEnabled; }
|
||||||
|
float sharpenAmount() const { return m_sharpenAmount; }
|
||||||
|
float brightness() const { return m_brightness; }
|
||||||
|
bool rawEnabled() const { return m_rawEnabled; }
|
||||||
|
bool compareEnabled() const { return m_compareEnabled; }
|
||||||
|
QImage grabProcessedFrame() { return grabFramebuffer(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent* event) override;
|
void initializeGL() override;
|
||||||
|
void resizeGL(int w, int h) override;
|
||||||
|
void paintGL() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QPixmap m_pixmap;
|
void uploadPendingFrame();
|
||||||
|
void drawNoSignal();
|
||||||
|
|
||||||
|
QOpenGLShaderProgram* m_program = nullptr;
|
||||||
|
QOpenGLTexture* m_texture = nullptr;
|
||||||
|
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
|
||||||
|
QImage m_pendingImage; // Wartet auf Upload im GL-Thread (paintGL)
|
||||||
|
bool m_pendingDirty = false;
|
||||||
|
|
||||||
|
QSize m_imageSize; // Auflösung des zuletzt hochgeladenen Frames
|
||||||
|
bool m_hasFrame = false;
|
||||||
|
bool m_smoothEnabled = true; // Bildglättung: Chroma/Luma-Rekonstruktion gegen Scaler-Artefakte
|
||||||
|
bool m_retroEnabled = false; // Retro-Optik: Scanlines/Maske/Wölbung (Nostalgie-Look)
|
||||||
|
float m_sharpenAmount = 0.0f; // Nachschärfen (Unsharp Mask): 0 = aus
|
||||||
|
float m_brightness = 1.0f; // Gamma-Faktor: 1.0 = neutral, >1 heller, <1 dunkler
|
||||||
|
bool m_rawEnabled = false; // Diagnose: umgeht ALLES (Studio-Range, Kontrast, Sättigung, Gamma)
|
||||||
|
bool m_compareEnabled = false; // Live-Vergleich: links roh, rechts bearbeitet
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr float kBrightnessMin = 0.4f;
|
||||||
|
static constexpr float kBrightnessMax = 3.2f;
|
||||||
|
static constexpr float kBrightnessStep = 0.1f;
|
||||||
|
static constexpr float kSharpenMin = 0.0f;
|
||||||
|
static constexpr float kSharpenMax = 20.0f;
|
||||||
|
static constexpr float kSharpenStep = 0.5f;
|
||||||
|
static constexpr float kSharpenDefault = 2.5f; // Wert für Taste X (Quick-Toggle)
|
||||||
};
|
};
|
||||||
|
|||||||
47
src/main.cpp
47
src/main.cpp
@@ -1,15 +1,24 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QSurfaceFormat>
|
||||||
|
|
||||||
#include "DeviceDialog.h"
|
#include "DeviceDialog.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
|
// OpenGL 3.3 Core für die Bildglättungs-/Retro-Optik-Shader in VideoWidget.
|
||||||
|
// Muss vor der QApplication gesetzt werden.
|
||||||
|
QSurfaceFormat fmt;
|
||||||
|
fmt.setVersion(3, 3);
|
||||||
|
fmt.setProfile(QSurfaceFormat::CoreProfile);
|
||||||
|
fmt.setSwapInterval(1);
|
||||||
|
QSurfaceFormat::setDefaultFormat(fmt);
|
||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
app.setApplicationName("HDMIViewer");
|
app.setApplicationName("HDMIViewer");
|
||||||
app.setApplicationVersion("1.2");
|
app.setApplicationVersion("1.4.0");
|
||||||
app.setOrganizationName("hdmi-viewer");
|
app.setOrganizationName("hdmi-viewer");
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
@@ -19,6 +28,12 @@ int main(int argc, char* argv[])
|
|||||||
dlg.setPreselectedVideo(settings.value("device/video").toString());
|
dlg.setPreselectedVideo(settings.value("device/video").toString());
|
||||||
dlg.setPreselectedAudio(settings.value("device/audio", "default").toString());
|
dlg.setPreselectedAudio(settings.value("device/audio", "default").toString());
|
||||||
dlg.setPreselectedFps(settings.value("device/fps", 30).toInt());
|
dlg.setPreselectedFps(settings.value("device/fps", 30).toInt());
|
||||||
|
dlg.setPreselectedSmoothMode(settings.value("display/smooth", true).toBool());
|
||||||
|
dlg.setPreselectedRetroMode(settings.value("display/retro", false).toBool());
|
||||||
|
dlg.setPreselectedSharpenAmount(settings.value("display/sharpen", 0.0).toFloat());
|
||||||
|
dlg.setPreselectedBrightness(settings.value("display/brightness", 1.0).toFloat());
|
||||||
|
dlg.setPreselectedHwContrast(settings.value("hardware/contrast", -1).toInt());
|
||||||
|
dlg.setPreselectedHwSaturation(settings.value("hardware/saturation", -1).toInt());
|
||||||
|
|
||||||
if (dlg.exec() != QDialog::Accepted)
|
if (dlg.exec() != QDialog::Accepted)
|
||||||
break;
|
break;
|
||||||
@@ -26,6 +41,18 @@ int main(int argc, char* argv[])
|
|||||||
const QString videoDevice = dlg.selectedVideoDevice();
|
const QString videoDevice = dlg.selectedVideoDevice();
|
||||||
const QString audioDevice = dlg.selectedAudioDevice();
|
const QString audioDevice = dlg.selectedAudioDevice();
|
||||||
const int fps = dlg.selectedFps();
|
const int fps = dlg.selectedFps();
|
||||||
|
const bool smoothMode = dlg.selectedSmoothMode();
|
||||||
|
const bool retroMode = dlg.selectedRetroMode();
|
||||||
|
const float sharpenAmt = dlg.selectedSharpenAmount();
|
||||||
|
const float brightness = dlg.selectedBrightness();
|
||||||
|
const int hwContrast = dlg.selectedHwContrast();
|
||||||
|
const int hwSaturation = dlg.selectedHwSaturation();
|
||||||
|
// Für die Taste [X] (Quick-Toggle 0 ↔ letzter Wert) zur Laufzeit:
|
||||||
|
// Falls im Dialog ein Wert >0 gesetzt wurde, ist das gleichzeitig
|
||||||
|
// der zu merkende "letzte" Wert.
|
||||||
|
const float lastSharpen = sharpenAmt > 0.0f
|
||||||
|
? sharpenAmt
|
||||||
|
: settings.value("display/sharpen_last", 2.5).toFloat();
|
||||||
|
|
||||||
if (videoDevice.isEmpty()) {
|
if (videoDevice.isEmpty()) {
|
||||||
QMessageBox::critical(nullptr, "Fehler",
|
QMessageBox::critical(nullptr, "Fehler",
|
||||||
@@ -36,9 +63,25 @@ int main(int argc, char* argv[])
|
|||||||
settings.setValue("device/video", videoDevice);
|
settings.setValue("device/video", videoDevice);
|
||||||
settings.setValue("device/audio", audioDevice);
|
settings.setValue("device/audio", audioDevice);
|
||||||
settings.setValue("device/fps", fps);
|
settings.setValue("device/fps", fps);
|
||||||
|
settings.setValue("display/smooth", smoothMode);
|
||||||
|
settings.setValue("display/retro", retroMode);
|
||||||
|
settings.setValue("display/sharpen", static_cast<double>(sharpenAmt));
|
||||||
|
settings.setValue("display/brightness", static_cast<double>(brightness));
|
||||||
|
if (sharpenAmt > 0.0f)
|
||||||
|
settings.setValue("display/sharpen_last", static_cast<double>(sharpenAmt));
|
||||||
|
// -1 = "unverändert" -> kein expliziter Nutzerwert, Regler bleibt
|
||||||
|
// beim Öffnen unangetastet (siehe CaptureThread::applyHardwareControls)
|
||||||
|
if (hwContrast >= 0)
|
||||||
|
settings.setValue("hardware/contrast", hwContrast);
|
||||||
|
else
|
||||||
|
settings.remove("hardware/contrast");
|
||||||
|
if (hwSaturation >= 0)
|
||||||
|
settings.setValue("hardware/saturation", hwSaturation);
|
||||||
|
else
|
||||||
|
settings.remove("hardware/saturation");
|
||||||
settings.sync();
|
settings.sync();
|
||||||
|
|
||||||
MainWindow win(videoDevice, audioDevice, fps);
|
MainWindow win(videoDevice, audioDevice, fps, smoothMode, retroMode, brightness, sharpenAmt, lastSharpen);
|
||||||
win.show();
|
win.show();
|
||||||
app.exec();
|
app.exec();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user