diff --git a/.github/workflows/apt/amd64.json b/.github/workflows/apt/amd64.json
index 8c8e4e47..9e0cce1b 100644
--- a/.github/workflows/apt/amd64.json
+++ b/.github/workflows/apt/amd64.json
@@ -2,64 +2,64 @@
{
"distribution": "Bionic",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev",
- "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg, libcec4",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev",
+ "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Ubuntu 18.04 (Bionic Beaver) (amd64)"
},
{
"distribution": "Focal",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
- "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg, libcec4",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
+ "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Ubuntu 20.04 (Focal Fossa) (amd64)"
},
{
"distribution": "Jammy",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
- "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
+ "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Ubuntu 22.04 (Jammy Jellyfish) (amd64)"
},
{
"distribution": "Kinetic",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
- "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
+ "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Ubuntu 22.10 (Kinetic Kudu) (amd64)"
},
{
"distribution": "Stretch",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev",
- "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg0, libcec4",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev",
+ "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg0, libcec4",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Debian 9.x (Stretch) (amd64)"
},
{
"distribution": "Buster",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
- "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec4",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
+ "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Debian 10.x (Buster) (amd64)"
},
{
"distribution": "Bullseye",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
- "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
+ "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Debian 11.x (Bullseye) (amd64)"
},
{
"distribution": "Bookworm",
"architecture": "amd64",
- "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
- "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6",
+ "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev",
+ "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6",
"cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release",
"description": "Debian 12.x (Bookworm) (amd64)"
}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index a2c530b1..ac3b57ba 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -32,7 +32,7 @@ jobs:
if: ${{ matrix.language == 'cpp' }}
run: |
sudo apt-get update
- sudo apt-get install --yes git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libturbojpeg0-dev libjpeg-dev libssl-dev
+ sudo apt-get install --yes git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
diff --git a/.gitignore b/.gitignore
index f86f420b..029eb5ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ libsrc/flatbufserver/hyperion_request_generated.h
# Ignore
.vs/*
CMakeSettings.json
+/out
# Allow
!.vs/launch.vs.json
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 68e791bc..e0c45882 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -67,6 +67,7 @@ SET ( DEFAULT_MF OFF )
SET ( DEFAULT_OSX OFF )
SET ( DEFAULT_QT ON )
SET ( DEFAULT_V4L2 OFF )
+SET ( DEFAULT_AUDIO ON )
SET ( DEFAULT_X11 OFF )
SET ( DEFAULT_XCB OFF )
@@ -172,8 +173,10 @@ if ( "${PLATFORM}" MATCHES "osx" )
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${SUBDIRPY})
include_directories("/opt/X11/include/")
- SET ( DEFAULT_OSX ON )
- SET ( DEFAULT_DEV_USB_HID ON )
+ SET ( DEFAULT_OSX ON )
+ SET ( DEFAULT_AUDIO OFF )
+ SET ( DEFAULT_DEV_USB_HID ON )
+
elseif ( "${PLATFORM}" MATCHES "rpi" )
SET ( DEFAULT_DISPMANX ON )
SET ( DEFAULT_DEV_WS281XPWM ON )
@@ -222,6 +225,7 @@ if (HYPERION_LIGHT)
SET ( DEFAULT_OSX OFF )
SET ( DEFAULT_QT OFF )
SET ( DEFAULT_V4L2 OFF )
+ SET ( DEFAULT_AUDIO OFF )
SET ( DEFAULT_X11 OFF )
SET ( DEFAULT_XCB OFF )
@@ -273,6 +277,11 @@ message(STATUS "ENABLE_V4L2 = ${ENABLE_V4L2}")
option(ENABLE_X11 "Enable the X11 grabber" ${DEFAULT_X11})
message(STATUS "ENABLE_X11 = ${ENABLE_X11}")
+option(ENABLE_AUDIO "Enable the AUDIO grabber" ${DEFAULT_AUDIO})
+message(STATUS "ENABLE_AUDIO = ${ENABLE_AUDIO}")
+
+option(ENABLE_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_WS281XPWM} )
+message(STATUS "ENABLE_WS281XPWM = ${ENABLE_WS281XPWM}")
option(ENABLE_XCB "Enable the XCB grabber" ${DEFAULT_XCB})
message(STATUS "ENABLE_XCB = ${ENABLE_XCB}")
diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in
index 4f49658c..324b703b 100644
--- a/HyperionConfig.h.in
+++ b/HyperionConfig.h.in
@@ -9,6 +9,10 @@
// Define to enable the DirectX grabber
#cmakedefine ENABLE_DX
+// Define to enable the framebuffer grabber
+// Define to enable the Audio grabber
+#cmakedefine ENABLE_AUDIO
+
// Define to enable the Framebuffer grabber
#cmakedefine ENABLE_FB
diff --git a/assets/webconfig/content/dashboard.html b/assets/webconfig/content/dashboard.html
index 77b3e950..84b08f72 100644
--- a/assets/webconfig/content/dashboard.html
+++ b/assets/webconfig/content/dashboard.html
@@ -42,6 +42,14 @@
+
+ |
+ Audio-Grabber |
+
+ disabled
+
+ |
+
@@ -135,6 +143,7 @@
+
diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json
index 9f56ef26..00af79eb 100644
--- a/assets/webconfig/i18n/en.json
+++ b/assets/webconfig/i18n/en.json
@@ -51,6 +51,7 @@
"conf_grabber_fg_intro": "Screen capture is your local system capture as input source, Hyperion is installed on.",
"conf_grabber_inst_grabber_config_info": "Configure your capturing hardware devices to be used by the instance in advance",
"conf_grabber_v4l_intro": "USB capture is a (capture) device connected via USB which is used to input source pictures for processing.",
+ "conf_grabber_audio_intro": "Audio capture utilizes an audio input device as the source for visualization.",
"conf_helptable_expl": "Explanation",
"conf_helptable_option": "Option",
"conf_leds_config_error": "Error in LED/LED layout configuration",
@@ -427,6 +428,8 @@
"edt_conf_instC_systemEnable_title": "Enable screen capture",
"edt_conf_instC_v4lEnable_expl": "Enables the USB capture for this LED hardware instance",
"edt_conf_instC_v4lEnable_title": "Enable USB capture",
+ "edt_conf_instC_audioEnable_expl": "Enables the Audio capture for this led hardware instance",
+ "edt_conf_instC_audioEnable_title": "Enable Audio capture",
"edt_conf_instC_video_grabber_device_expl": "The video capture device used",
"edt_conf_instC_video_grabber_device_title": "Video capture device",
"edt_conf_instCapture_heading_title": "Capture Devices",
@@ -527,6 +530,27 @@
"edt_conf_v4l2_hardware_set_defaults_tip": "Set device's default values for brightness, contrast, hue and saturation",
"edt_conf_v4l2_noSignalCounterThreshold_title": "Signal Counter Threshold",
"edt_conf_v4l2_noSignalCounterThreshold_expl": "Count of frames (check that with grabber's current FPS mode) after which the no signal is triggered",
+ "edt_conf_audio_device_expl": "Selected audio input device",
+ "edt_conf_audio_device_title": "Audio Device",
+ "edt_conf_audio_effects_expl": "Select an effect on how the audio signal is transformed to",
+ "edt_conf_audio_effects_title": "Audio Effects",
+ "edt_conf_audio_effect_enum_vumeter": "VU-Meter",
+ "edt_conf_audio_effect_hotcolor_expl": "Hot Color",
+ "edt_conf_audio_effect_hotcolor_title": "Hot Color",
+ "edt_conf_audio_effect_multiplier_expl": "Audio Signal Value multiplier",
+ "edt_conf_audio_effect_multiplier_title": "Multiplier",
+ "edt_conf_audio_effect_safecolor_expl": "Safe Color",
+ "edt_conf_audio_effect_safecolor_title": "Safe Color",
+ "edt_conf_audio_effect_safevalue_expl": "Safe Threshold",
+ "edt_conf_audio_effect_safevalue_title": "Safe Threshold",
+ "edt_conf_audio_effect_set_defaults": "Reset to default values",
+ "edt_conf_audio_effect_tolerance_expl": "Tolerance used when auto calculating a signal multipler from 0-100",
+ "edt_conf_audio_effect_tolerance_title": "Tolerance",
+ "edt_conf_audio_effect_warncolor_expl": "Warning Color",
+ "edt_conf_audio_effect_warncolor_title": "Warning Color",
+ "edt_conf_audio_effect_warnvalue_expl": "Warning Threshold",
+ "edt_conf_audio_effect_warnvalue_title": "Warning Threshold",
+ "edt_conf_audio_heading_title": "Audio Capture",
"edt_conf_webc_crtPath_expl": "Path to the certification file (format should be PEM)",
"edt_conf_webc_crtPath_title": "Certificate path",
"edt_conf_webc_docroot_expl": "Local webinterface root path (just for webui developer)",
@@ -876,6 +900,7 @@
"general_comp_PROTOSERVER": "Protocol Buffers Server",
"general_comp_SMOOTHING": "Smoothing",
"general_comp_V4L": "Capture USB-Input",
+ "general_comp_AUDIO": "Audio Capture",
"general_country_cn": "China",
"general_country_de": "Germany",
"general_country_es": "Spain",
diff --git a/assets/webconfig/index.html b/assets/webconfig/index.html
index bb1a7d0b..9249eb39 100644
--- a/assets/webconfig/index.html
+++ b/assets/webconfig/index.html
@@ -24,8 +24,10 @@
-
-
+
diff --git a/assets/webconfig/js/content_dashboard.js b/assets/webconfig/js/content_dashboard.js
index 1fca3574..cc4c4ffa 100644
--- a/assets/webconfig/js/content_dashboard.js
+++ b/assets/webconfig/js/content_dashboard.js
@@ -58,7 +58,8 @@ $(document).ready(function () {
if (components[idx].name != "ALL") {
if ((components[idx].name === "FORWARDER" && window.currentHyperionInstance != 0) ||
(components[idx].name === "GRABBER" && !window.serverConfig.framegrabber.enable) ||
- (components[idx].name === "V4L" && !window.serverConfig.grabberV4L2.enable))
+ (components[idx].name === "V4L" && !window.serverConfig.grabberV4L2.enable) ||
+ (components[idx].name === "AUDIO" && !window.serverConfig.grabberAudio.enable))
continue;
var comp_enabled = components[idx].enabled ? "checked" : "";
@@ -104,8 +105,9 @@ $(document).ready(function () {
var screenGrabberAvailable = (window.serverInfo.grabbers.screen.available.length !== 0);
var videoGrabberAvailable = (window.serverInfo.grabbers.video.available.length !== 0);
+ const audioGrabberAvailable = (window.serverInfo.grabbers.audio.available.length !== 0);
- if (screenGrabberAvailable || videoGrabberAvailable) {
+ if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) {
if (screenGrabberAvailable) {
var screenGrabber = window.serverConfig.framegrabber.enable ? $.i18n('general_enabled') : $.i18n('general_disabled');
@@ -120,6 +122,13 @@ $(document).ready(function () {
} else {
$("#dash_video_grabber_row").hide();
}
+
+ if (audioGrabberAvailable) {
+ const audioGrabber = window.serverConfig.grabberAudio.enable ? $.i18n('general_enabled') : $.i18n('general_disabled');
+ $('#dash_audio_grabber').html(audioGrabber);
+ } else {
+ $("#dash_audio_grabber_row").hide();
+ }
} else {
$("#dash_capture_hw").hide();
}
diff --git a/assets/webconfig/js/content_grabber.js b/assets/webconfig/js/content_grabber.js
index 80304cb7..20da6030 100755
--- a/assets/webconfig/js/content_grabber.js
+++ b/assets/webconfig/js/content_grabber.js
@@ -4,9 +4,11 @@ $(document).ready(function () {
var screenGrabberAvailable = (window.serverInfo.grabbers.screen.available.length !== 0);
var videoGrabberAvailable = (window.serverInfo.grabbers.video.available.length !== 0);
+ const audioGrabberAvailable = (window.serverInfo.grabbers.audio.available.length !== 0);
var CEC_ENABLED = (jQuery.inArray("cec", window.serverInfo.services) !== -1);
var conf_editor_video = null;
+ var conf_editor_audio = null;
var conf_editor_screen = null;
var configuredDevice = "";
@@ -38,6 +40,22 @@ $(document).ready(function () {
}
}
+ // Audio-Grabber
+ if (audioGrabberAvailable) {
+ $('#conf_cont').append(createRow('conf_cont_audio'));
+ $('#conf_cont_audio').append(createOptPanel('fa-volume', $.i18n("edt_conf_audio_heading_title"), 'editor_container_audiograbber', 'btn_submit_audiograbber', 'panel-system', 'audiograbberPanelId'));
+
+ if (storedAccess === 'expert') {
+ const conf_cont_audio_footer = document.getElementById("editor_container_audiograbber").nextElementSibling;
+ $(conf_cont_audio_footer).prepend(' ');
+ }
+
+ if (window.showOptHelp) {
+ $('#conf_cont_audio').append(createHelpTable(window.schema.grabberAudio.properties, $.i18n("edt_conf_audio_heading_title"), "audiograbberHelpPanelId"));
+ }
+ }
+
JSONEditor.defaults.custom_validators.push(function (schema, value, path) {
var errors = [];
@@ -694,6 +712,121 @@ $(document).ready(function () {
});
}
+ // External Input Sources (Audio-Grabbers)
+ if (audioGrabberAvailable) {
+
+ conf_editor_audio = createJsonEditor('editor_container_audiograbber', {
+ grabberAudio: window.schema.grabberAudio
+ }, true, true);
+
+ conf_editor_audio.on('ready', () => {
+ // Trigger conf_editor_audio.watch - 'root.grabberAudio.enable'
+ const audioEnable = window.serverConfig.grabberAudio.enable;
+ conf_editor_audio.getEditor("root.grabberAudio.enable").setValue(audioEnable);
+ });
+
+ conf_editor_audio.on('change', () => {
+
+ // Validate the current editor's content
+ if (!conf_editor_audio.validate().length) {
+ const deviceSelected = conf_editor_audio.getEditor("root.grabberAudio.available_devices").getValue();
+ switch (deviceSelected) {
+ case "SELECT":
+ showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], false);
+ break;
+ case "NONE":
+ showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], false);
+ break;
+ default:
+ window.readOnlyMode ? $('#btn_submit_audiograbber').prop('disabled', true) : $('#btn_submit_audiograbber').prop('disabled', false);
+ break;
+ }
+ }
+ else {
+ $('#btn_submit_audiograbber').prop('disabled', true);
+ }
+ });
+
+ // Enable
+ conf_editor_audio.watch('root.grabberAudio.enable', () => {
+
+ const audioEnable = conf_editor_audio.getEditor("root.grabberAudio.enable").getValue();
+ if (audioEnable)
+ {
+ showInputOptionsForKey(conf_editor_audio, "grabberAudio", "enable", true);
+
+ $('#btn_audiograbber_set_effect_defaults').show();
+
+ if (window.showOptHelp) {
+ $('#audiograbberHelpPanelId').show();
+ }
+
+ discoverInputSources("audio");
+ }
+ else
+ {
+ $('#btn_submit_audiograbber').prop('disabled', false);
+ $('#btn_audiograbber_set_effect_defaults').hide();
+ showInputOptionsForKey(conf_editor_audio, "grabberAudio", "enable", false);
+ $('#audiograbberHelpPanelId').hide();
+ }
+ });
+
+ // Available Devices
+ conf_editor_audio.watch('root.grabberAudio.available_devices', () => {
+ const deviceSelected = conf_editor_audio.getEditor("root.grabberAudio.available_devices").getValue();
+
+ if (deviceSelected === "SELECT" || deviceSelected === "NONE" || deviceSelected === "") {
+ $('#btn_submit_audiograbber').prop('disabled', true);
+ showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], false);
+ }
+ else
+ {
+ showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], true);
+
+ const deviceProperties = getPropertiesOfDevice("audio", deviceSelected);
+
+ //Update hidden input element
+ conf_editor_audio.getEditor("root.grabberAudio.device").setValue(deviceProperties.device);
+
+ //Enfore configured JSON-editor dependencies
+ conf_editor_audio.notifyWatchers("root.grabberAudio.audioEffect");
+
+ //Enable set defaults button
+ $('#btn_audiograbber_set_effect_defaults').prop('disabled', false);
+
+ if (conf_editor_audio.validate().length && !window.readOnlyMode) {
+ $('#btn_submit_audiograbber').prop('disabled', false);
+ }
+ }
+ });
+
+ $('#btn_submit_audiograbber').off().on('click', function () {
+ const saveOptions = conf_editor_audio.getValue();
+
+ const instCaptOptions = window.serverConfig.instCapture;
+ instCaptOptions.audioEnable = true;
+ saveOptions.instCapture = instCaptOptions;
+
+ requestWriteConfig(saveOptions);
+ });
+
+ // ------------------------------------------------------------------
+
+ $('#btn_audiograbber_set_effect_defaults').off().on('click', function () {
+ const currentEffect = conf_editor_audio.getEditor("root.grabberAudio.audioEffect").getValue();
+ var effectEditor = conf_editor_audio.getEditor("root.grabberAudio." + currentEffect);
+ var defaultProperties = effectEditor.schema.defaultProperties;
+
+ var default_values = {};
+ for (const item of defaultProperties) {
+
+ default_values[item] = effectEditor.schema.properties[item].default;
+ }
+ effectEditor.setValue(default_values);
+ });
+ }
+
// ------------------------------------------------------------------
//////////////////////////////////////////////////
@@ -706,6 +839,9 @@ $(document).ready(function () {
if (videoGrabberAvailable) {
createHint("intro", $.i18n('conf_grabber_v4l_intro'), "editor_container_videograbber");
}
+ if (audioGrabberAvailable) {
+ createHint("intro", $.i18n('conf_grabber_audio_intro'), "editor_container_audiograbber");
+ }
}
removeOverlay();
@@ -773,6 +909,38 @@ $(document).ready(function () {
}
};
+ // build dynamic audio input enum
+ const updateAudioSourcesList = function (type, discoveryInfo) {
+ const enumVals = [];
+ const enumTitelVals = [];
+ let enumDefaultVal = "";
+ let addSelect = false;
+
+ if (jQuery.isEmptyObject(discoveryInfo)) {
+ enumVals.push("NONE");
+ enumTitelVals.push($.i18n('edt_conf_grabber_discovered_none'));
+ }
+ else {
+ for (const device of discoveryInfo) {
+ enumVals.push(device.device_name);
+ }
+ conf_editor_audio.getEditor('root.grabberAudio').enable();
+ configuredDevice = window.serverConfig.grabberAudio.available_devices;
+
+ if ($.inArray(configuredDevice, enumVals) != -1) {
+ enumDefaultVal = configuredDevice;
+ }
+ else {
+ addSelect = true;
+ }
+ }
+
+ if (enumVals.length > 0) {
+ updateJsonEditorSelection(conf_editor_audio, 'root.grabberAudio',
+ 'available_devices', {}, enumVals, enumTitelVals, enumDefaultVal, addSelect, false);
+ }
+ };
+
async function discoverInputSources(type, params) {
const result = await requestInputSourcesDiscovery(type, params);
@@ -782,7 +950,8 @@ $(document).ready(function () {
}
else {
discoveryResult = {
- "video_sources": []
+ "video_sources": [],
+ "audio_soruces": []
};
}
@@ -799,6 +968,12 @@ $(document).ready(function () {
updateVideoSourcesList(type, discoveredInputSources.video);
}
break;
+ case "audio":
+ discoveredInputSources.audio = discoveryResult.audio_sources;
+ if (audioGrabberAvailable) {
+ updateAudioSourcesList(type, discoveredInputSources.audio);
+ }
+ break;
}
}
diff --git a/assets/webconfig/js/content_instcapture.js b/assets/webconfig/js/content_instcapture.js
index fd399112..abe00081 100644
--- a/assets/webconfig/js/content_instcapture.js
+++ b/assets/webconfig/js/content_instcapture.js
@@ -3,6 +3,7 @@ $(document).ready(function () {
var screenGrabberAvailable = (window.serverInfo.grabbers.screen.available.length !== 0);
var videoGrabberAvailable = (window.serverInfo.grabbers.video.available.length !== 0);
+ const audioGrabberAvailable = (window.serverInfo.grabbers.audio.available.length !== 0);
var BOBLIGHT_ENABLED = (jQuery.inArray("boblight", window.serverInfo.services) !== -1);
@@ -15,7 +16,7 @@ $(document).ready(function () {
// Instance Capture
if (window.showOptHelp) {
- if (screenGrabberAvailable || videoGrabberAvailable) {
+ if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) {
$('#conf_cont').append(createRow('conf_cont_instCapt'));
$('#conf_cont_instCapt').append(createOptPanel('fa-camera', $.i18n("edt_conf_instCapture_heading_title"), 'editor_container_instCapt', 'btn_submit_instCapt', ''));
$('#conf_cont_instCapt').append(createHelpTable(window.schema.instCapture.properties, $.i18n("edt_conf_instCapture_heading_title")));
@@ -29,7 +30,7 @@ $(document).ready(function () {
}
else {
$('#conf_cont').addClass('row');
- if (screenGrabberAvailable || videoGrabberAvailable) {
+ if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) {
$('#conf_cont').append(createOptPanel('fa-camera', $.i18n("edt_conf_instCapture_heading_title"), 'editor_container_instCapt', 'btn_submit_instCapt', ''));
}
if (BOBLIGHT_ENABLED) {
@@ -37,7 +38,7 @@ $(document).ready(function () {
}
}
- if (screenGrabberAvailable || videoGrabberAvailable) {
+ if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) {
// Instance Capture
conf_editor_instCapt = createJsonEditor('editor_container_instCapt', {
@@ -81,12 +82,29 @@ $(document).ready(function () {
showInputOptionForItem(conf_editor_instCapt, "instCapture", "v4lPriority", false);
}
+ if (audioGrabberAvailable) {
+ if (!window.serverConfig.grabberAudio.enable) {
+ conf_editor_instCapt.getEditor("root.instCapture.audioEnable").setValue(false);
+ conf_editor_instCapt.getEditor("root.instCapture.audioEnable").disable();
+ }
+ else {
+ conf_editor_instCapt.getEditor("root.instCapture.audioEnable").setValue(window.serverConfig.instCapture.audioEnable);
+
+ }
+ } else {
+ showInputOptionForItem(conf_editor_instCapt, "instCapture", "audioGrabberDevice", false);
+ showInputOptionForItem(conf_editor_instCapt, "instCapture", "audioEnable", false);
+ showInputOptionForItem(conf_editor_instCapt, "instCapture", "audioPriority", false);
+ }
+
});
conf_editor_instCapt.on('change', function () {
if (!conf_editor_instCapt.validate().length) {
- if (!window.serverConfig.framegrabber.enable && !window.serverConfig.grabberV4L2.enable) {
+ if (!window.serverConfig.framegrabber.enable &&
+ !window.serverConfig.grabberV4L2.enable &&
+ !window.serverConfig.grabberAudio.enable) {
$('#btn_submit_instCapt').prop('disabled', true);
} else {
window.readOnlyMode ? $('#btn_submit_instCapt').prop('disabled', true) : $('#btn_submit_instCapt').prop('disabled', false);
@@ -130,6 +148,23 @@ $(document).ready(function () {
}
});
+ conf_editor_instCapt.watch('root.instCapture.audioEnable', () => {
+ const audioEnable = conf_editor_instCapt.getEditor("root.instCapture.audioEnable").getValue();
+ if (audioEnable) {
+ conf_editor_instCapt.getEditor("root.instCapture.audioGrabberDevice").setValue(window.serverConfig.grabberAudio.available_devices);
+ conf_editor_instCapt.getEditor("root.instCapture.audioGrabberDevice").disable();
+ showInputOptions("instCapture", ["audioGrabberDevice"], true);
+ showInputOptions("instCapture", ["audioPriority"], true);
+ }
+ else {
+ if (!window.serverConfig.grabberAudio.enable) {
+ conf_editor_instCapt.getEditor("root.instCapture.audioEnable").disable();
+ }
+ showInputOptions("instCapture", ["audioGrabberDevice"], false);
+ showInputOptions("instCapture", ["audioPriority"], false);
+ }
+ });
+
$('#btn_submit_instCapt').off().on('click', function () {
requestWriteConfig(conf_editor_instCapt.getValue());
});
diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js
index e689c885..09fc475c 100644
--- a/assets/webconfig/js/content_remote.js
+++ b/assets/webconfig/js/content_remote.js
@@ -154,6 +154,9 @@ $(document).ready(function () {
case "V4L":
owner = $.i18n('general_comp_V4L') + ': (' + owner + ')';
break;
+ case "AUDIO":
+ owner = $.i18n('general_comp_AUDIO') + ': (' + owner + ')';
+ break;
case "BOBLIGHTSERVER":
owner = $.i18n('general_comp_BOBLIGHTSERVER');
break;
@@ -219,7 +222,8 @@ $(document).ready(function () {
for (const comp of components) {
if (comp.name === "ALL" || (comp.name === "FORWARDER" && window.currentHyperionInstance != 0) ||
(comp.name === "GRABBER" && !window.serverConfig.framegrabber.enable) ||
- (comp.name === "V4L" && !window.serverConfig.grabberV4L2.enable))
+ (comp.name === "V4L" && !window.serverConfig.grabberV4L2.enable) ||
+ (comp.name === "AUDIO" && !window.serverConfig.grabberAudio.enable))
continue;
const enable_style = (comp.enabled ? "checked" : "");
diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js
index e1a2ae5b..d2a998dc 100644
--- a/assets/webconfig/js/ui_utils.js
+++ b/assets/webconfig/js/ui_utils.js
@@ -1220,6 +1220,7 @@ function getSystemInfo() {
//info += '- Log lvl: ' + window.serverConfig.logger.level + '\n';
info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n';
info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n';
+ info += '- Avail Audio Cap.: ' + window.serverInfo.grabbers.audio.available + '\n';
info += '- Avail Services: ' + window.serverInfo.services + '\n';
info += '- Config path: ' + shy.rootPath + '\n';
info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n';
@@ -1317,9 +1318,9 @@ function showInputOptionsForKey(editor, item, showForKeys, state) {
}
}
- for (var key in editor.schema.properties[item].properties) {
+ for (let key in editor.schema.properties[item].properties) {
if ($.inArray(key, keysToshow) === -1) {
- var accessLevel = editor.schema.properties[item].properties[key].access;
+ const accessLevel = editor.schema.properties[item].properties[key].access;
var hidden = false;
if (editor.schema.properties[item].properties[key].options) {
diff --git a/cmake/packages.cmake b/cmake/packages.cmake
index 1dd8f6a6..046cefe7 100644
--- a/cmake/packages.cmake
+++ b/cmake/packages.cmake
@@ -155,6 +155,9 @@ if(ENABLE_FLATBUF_CONNECT)
if(ENABLE_V4L2)
SET ( CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} "hyperion_v4l2" )
endif()
+ if(ENABLE_AUDIO)
+ SET ( CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} "hyperion_audio" )
+ endif()
if(ENABLE_X11)
SET ( CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} "hyperion_x11" )
endif()
diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default
index a52f64ba..84218810 100644
--- a/config/hyperion.config.json.default
+++ b/config/hyperion.config.json.default
@@ -1,239 +1,236 @@
{
- "general" :
- {
- "name" : "My Hyperion Config",
- "configVersion": "configVersionValue",
- "previousVersion": "previousVersionValue",
- "watchedVersionBranch" : "Stable",
- "showOptHelp" : true
+ "general": {
+ "name": "My Hyperion Config",
+ "configVersion": "configVersionValue",
+ "previousVersion": "previousVersionValue",
+ "watchedVersionBranch": "Stable",
+ "showOptHelp": true
},
- "logger" :
- {
- "level" : "warn"
+ "logger": {
+ "level": "warn"
},
- "device" :
- {
- "type" : "file",
- "hardwareLedCount" : 1,
- "autoStart" : true,
- "output" : "/dev/null",
- "colorOrder" : "rgb",
- "latchTime" : 0,
+ "device": {
+ "type": "file",
+ "hardwareLedCount": 1,
+ "autoStart": true,
+ "output": "/dev/null",
+ "colorOrder": "rgb",
+ "latchTime": 0,
"rewriteTime": 0,
"enableAttempts": 6,
"enableAttemptsInterval": 15
},
- "color" :
- {
- "imageToLedMappingType" : "multicolor_mean",
- "channelAdjustment" :
- [
+ "color": {
+ "imageToLedMappingType": "multicolor_mean",
+ "channelAdjustment": [
{
- "id" : "default",
- "leds" : "*",
- "white" : [255,255,255],
- "red" : [255,0,0],
- "green" : [0,255,0],
- "blue" : [0,0,255],
- "cyan" : [0,255,255],
- "magenta" : [255,0,255],
- "yellow" : [255,255,0],
- "gammaRed" : 2.2,
- "gammaGreen" : 2.2,
- "gammaBlue" : 2.2,
- "backlightThreshold" : 0,
- "backlightColored" : false,
- "brightness" : 100,
- "brightnessCompensation" : 100,
- "saturationGain" : 1.0,
- "brightnessGain" : 1.0
+ "id": "default",
+ "leds": "*",
+ "white": [ 255, 255, 255 ],
+ "red": [ 255, 0, 0 ],
+ "green": [ 0, 255, 0 ],
+ "blue": [ 0, 0, 255 ],
+ "cyan": [ 0, 255, 255 ],
+ "magenta": [ 255, 0, 255 ],
+ "yellow": [ 255, 255, 0 ],
+ "gammaRed": 2.2,
+ "gammaGreen": 2.2,
+ "gammaBlue": 2.2,
+ "backlightThreshold": 0,
+ "backlightColored": false,
+ "brightness": 100,
+ "brightnessCompensation": 100,
+ "saturationGain": 1.0,
+ "brightnessGain": 1.0
}
]
},
- "smoothing" :
- {
- "enable" : true,
- "type" : "linear",
- "time_ms" : 200,
- "updateFrequency" : 25.0000,
- "interpolationRate" : 25.0000,
- "decay" : 1,
- "dithering" : false,
- "updateDelay" : 0
+ "smoothing": {
+ "enable": true,
+ "type": "linear",
+ "time_ms": 200,
+ "updateFrequency": 25.0000,
+ "interpolationRate": 25.0000,
+ "decay": 1,
+ "dithering": false,
+ "updateDelay": 0
},
- "grabberV4L2" :
- {
- "enable" : false,
- "device" : "none",
- "input" : 0,
- "encoding" : "NO_CHANGE",
- "width" : 0,
- "height" : 0,
- "fps" : 15,
- "flip" : "NO_CHANGE",
- "fpsSoftwareDecimation" : 0,
- "sizeDecimation" : 8,
- "cropLeft" : 0,
- "cropRight" : 0,
- "cropTop" : 0,
- "cropBottom" : 0,
- "redSignalThreshold" : 0,
- "greenSignalThreshold" : 100,
- "blueSignalThreshold" : 0,
- "signalDetection" : false,
- "noSignalCounterThreshold" : 200,
- "cecDetection" : false,
- "sDVOffsetMin" : 0.1,
- "sDVOffsetMax" : 0.9,
- "sDHOffsetMin" : 0.4,
- "sDHOffsetMax" : 0.46,
- "hardware_brightness" : 0,
- "hardware_contrast" : 0,
- "hardware_saturation" : 0,
- "hardware_hue" : 0
+ "grabberV4L2": {
+ "enable": false,
+ "device": "none",
+ "input": 0,
+ "encoding": "NO_CHANGE",
+ "width": 0,
+ "height": 0,
+ "fps": 15,
+ "flip": "NO_CHANGE",
+ "fpsSoftwareDecimation": 0,
+ "sizeDecimation": 8,
+ "cropLeft": 0,
+ "cropRight": 0,
+ "cropTop": 0,
+ "cropBottom": 0,
+ "redSignalThreshold": 0,
+ "greenSignalThreshold": 100,
+ "blueSignalThreshold": 0,
+ "signalDetection": false,
+ "noSignalCounterThreshold": 200,
+ "cecDetection": false,
+ "sDVOffsetMin": 0.1,
+ "sDVOffsetMax": 0.9,
+ "sDHOffsetMin": 0.4,
+ "sDHOffsetMax": 0.46,
+ "hardware_brightness": 0,
+ "hardware_contrast": 0,
+ "hardware_saturation": 0,
+ "hardware_hue": 0
},
- "framegrabber" :
- {
- "enable" : false,
- "device" : "auto",
- "input" : 0,
- "width" : 80,
- "height" : 45,
- "fps" : 10,
- "pixelDecimation" : 8,
- "cropLeft" : 0,
- "cropRight" : 0,
- "cropTop" : 0,
- "cropBottom" : 0
+ "grabberAudio": {
+ "enable": false,
+ "device": "auto",
+ "audioEffect": "vuMeter",
+ "vuMeter": {
+ "flip": "NO_CHANGE",
+ "hotColor": [ 255, 0, 0 ],
+ "multiplier": 1,
+ "safeColor": [ 0, 255, 0 ],
+ "safeValue": 45,
+ "tolerance": 5,
+ "warnColor": [ 255, 255, 0 ],
+ "warnValue": 80
+ }
},
- "blackborderdetector" :
- {
- "enable" : true,
- "threshold" : 5,
- "unknownFrameCnt" : 600,
- "borderFrameCnt" : 50,
- "maxInconsistentCnt" : 10,
- "blurRemoveCnt" : 1,
- "mode" : "default"
+ "framegrabber": {
+ "enable": false,
+ "device": "auto",
+ "input": 0,
+ "width": 80,
+ "height": 45,
+ "fps": 10,
+ "pixelDecimation": 8,
+ "cropLeft": 0,
+ "cropRight": 0,
+ "cropTop": 0,
+ "cropBottom": 0
},
- "foregroundEffect" :
- {
- "enable" : true,
- "type" : "effect",
- "color" : [0,0,255],
- "effect" : "Rainbow swirl fast",
- "duration_ms" : 3000
+ "blackborderdetector": {
+ "enable": true,
+ "threshold": 5,
+ "unknownFrameCnt": 600,
+ "borderFrameCnt": 50,
+ "maxInconsistentCnt": 10,
+ "blurRemoveCnt": 1,
+ "mode": "default"
},
- "backgroundEffect" :
- {
- "enable" : false,
- "type" : "effect",
- "color" : [255,138,0],
- "effect" : "Warm mood blobs"
+ "foregroundEffect": {
+ "enable": true,
+ "type": "effect",
+ "color": [ 0, 0, 255 ],
+ "effect": "Rainbow swirl fast",
+ "duration_ms": 3000
},
- "forwarder" :
- {
- "enable" : false,
- "jsonapi" : [],
- "flatbuffer" : []
+ "backgroundEffect": {
+ "enable": false,
+ "type": "effect",
+ "color": [ 255, 138, 0 ],
+ "effect": "Warm mood blobs"
},
- "jsonServer" :
- {
- "port" : 19444
+ "forwarder": {
+ "enable": false,
+ "jsonapi": [],
+ "flatbuffer": []
},
- "flatbufServer" :
- {
- "enable" : true,
- "port" : 19400,
- "timeout" : 5
+ "jsonServer": {
+ "port": 19444
},
- "protoServer" :
- {
- "enable" : true,
- "port" : 19445,
- "timeout" : 5
+ "flatbufServer": {
+ "enable": true,
+ "port": 19400,
+ "timeout": 5
},
- "boblightServer" :
- {
- "enable" : false,
- "port" : 19333,
- "priority" : 128
+ "protoServer": {
+ "enable": true,
+ "port": 19445,
+ "timeout": 5
},
- "webConfig" :
- {
- "document_root" : "",
- "port" : 8090,
- "sslPort" : 8092,
- "crtPath" : "",
- "keyPath" : "",
- "keyPassPhrase" : ""
+ "boblightServer": {
+ "enable": false,
+ "port": 19333,
+ "priority": 128
},
- "effects" :
- {
- "paths" : ["$ROOT/custom-effects"],
- "disable": [""]
+ "webConfig": {
+ "document_root": "",
+ "port": 8090,
+ "sslPort": 8092,
+ "crtPath": "",
+ "keyPath": "",
+ "keyPassPhrase": ""
},
- "instCapture" :
- {
- "systemEnable" : false,
- "systemGrabberDevice" : "NONE",
- "systemPriority" : 250,
- "v4lEnable" : false,
- "v4lGrabberDevice" : "NONE",
- "v4lPriority" : 240
+ "effects": {
+ "paths": [ "$ROOT/custom-effects" ],
+ "disable": [ "" ]
},
- "network" :
- {
- "internetAccessAPI" : false,
- "restirctedInternetAccessAPI" : false,
- "ipWhitelist" : [],
- "apiAuth" : true,
- "localApiAuth" : false,
+ "instCapture": {
+ "systemEnable": false,
+ "systemGrabberDevice": "NONE",
+ "systemPriority": 250,
+ "v4lEnable": false,
+ "v4lGrabberDevice": "NONE",
+ "v4lPriority": 240,
+ "audioEnable": false,
+ "audioGrabberDevice": "NONE",
+ "audioPriority": 230
+ },
+
+ "network": {
+ "internetAccessAPI": false,
+ "restirctedInternetAccessAPI": false,
+ "ipWhitelist": [],
+ "apiAuth": true,
+ "localApiAuth": false,
"localAdminAuth": true
},
- "ledConfig" :
- {
- "classic":
- {
- "top" : 1,
- "bottom" : 0,
- "left" : 0,
- "right" : 0,
- "glength" : 0,
- "gpos" : 0,
- "position" : 0,
- "reverse" : false,
- "hdepth" : 8,
- "vdepth" : 5,
- "overlap" : 0,
- "edgegap" : 0,
- "ptlh" : 0,
- "ptlv" : 0,
- "ptrh" : 100,
- "ptrv" : 0,
- "pblh" : 0,
- "pblv" : 100,
- "pbrh" : 100,
- "pbrv" : 100
- },
+ "ledConfig": {
+ "classic": {
+ "top": 1,
+ "bottom": 0,
+ "left": 0,
+ "right": 0,
+ "glength": 0,
+ "gpos": 0,
+ "position": 0,
+ "reverse": false,
+ "hdepth": 8,
+ "vdepth": 5,
+ "overlap": 0,
+ "edgegap": 0,
+ "ptlh": 0,
+ "ptlv": 0,
+ "ptrh": 100,
+ "ptrv": 0,
+ "pblh": 0,
+ "pblv": 100,
+ "pbrh": 100,
+ "pbrv": 100
+ },
"matrix": {
"ledshoriz": 1,
@@ -244,8 +241,7 @@
}
},
- "leds":
- [
+ "leds": [
{
"hmax": 1,
"hmin": 0,
diff --git a/doc/development/CompileHowto.md b/doc/development/CompileHowto.md
index f13f880d..f3b5502c 100644
--- a/doc/development/CompileHowto.md
+++ b/doc/development/CompileHowto.md
@@ -87,14 +87,14 @@ cd $HYPERION_HOME
```console
sudo apt-get update
-sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libturbojpeg0-dev libjpeg-dev libssl-dev
+sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev
```
**Ubuntu (22.04+) - Qt6 based**
```console
sudo apt-get update
-sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config
+sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config
```
**For Linux X11/XCB grabber support**
@@ -136,7 +136,7 @@ See [AUR](https://aur.archlinux.org/packages/?O=0&SeB=nd&K=hyperion&outdated=&SB
The following dependencies are needed to build hyperion.ng on fedora.
```console
sudo dnf -y groupinstall "Development Tools"
-sudo dnf install python3-devel qt-devel qt5-qtbase-devel qt5-qtserialport-devel xrandr xcb-util-image-devel qt5-qtx11extras-devel turbojpeg-devel libusb-devel xcb-util-devel dbus-devel openssl-devel fedora-packager rpmdevtools gcc libcec-devel
+sudo dnf install python3-devel qt-devel qt5-qtbase-devel qt5-qtserialport-devel xrandr xcb-util-image-devel qt5-qtx11extras-devel alsa-lib-devel turbojpeg-devel libusb-devel xcb-util-devel dbus-devel openssl-devel fedora-packager rpmdevtools gcc libcec-devel
```
After installing the dependencies, you can continue with the compile instructions later on this page (the more detailed way..).
diff --git a/include/db/SettingsTable.h b/include/db/SettingsTable.h
index 19bf1054..d7184869 100644
--- a/include/db/SettingsTable.h
+++ b/include/db/SettingsTable.h
@@ -111,7 +111,7 @@ public:
// server port services
list << "jsonServer" << "protoServer" << "flatbufServer" << "forwarder" << "webConfig" << "network"
// capture
- << "framegrabber" << "grabberV4L2"
+ << "framegrabber" << "grabberV4L2" << "grabberAudio"
// other
<< "logger" << "general";
diff --git a/include/grabber/AudioGrabber.h b/include/grabber/AudioGrabber.h
new file mode 100644
index 00000000..115f3aa8
--- /dev/null
+++ b/include/grabber/AudioGrabber.h
@@ -0,0 +1,196 @@
+#ifndef AUDIOGRABBER_H
+#define AUDIOGRABBER_H
+
+#include
+#include
+#include
+
+// Hyperion-utils includes
+#include
+#include
+#include
+
+///
+/// Base Audio Grabber Class
+///
+/// This class is extended by the windows audio grabber to provied DirectX9 access to the audio devices
+/// This class is extended by the linux audio grabber to provide ALSA access to the audio devices
+///
+/// @brief The DirectX9 capture implementation
+///
+class AudioGrabber : public Grabber
+{
+ Q_OBJECT
+ public:
+
+ ///
+ /// Device properties
+ ///
+ /// this structure holds the name, id, and inputs of the enumerated audio devices.
+ ///
+ struct DeviceProperties
+ {
+ QString name = QString();
+ QString id = QString();
+ QMultiMap inputs = QMultiMap();
+ };
+
+ AudioGrabber();
+ ~AudioGrabber() override;
+
+ ///
+ /// Start audio capturing session
+ ///
+ /// @returns true if successful
+ virtual bool start();
+
+ ///
+ /// Stop audio capturing session
+ ///
+ virtual void stop();
+
+ ///
+ /// Restart the audio capturing session
+ ///
+ void restart();
+
+ Logger* getLog();
+
+ ///
+ /// Set Device
+ ///
+ /// configures the audio device used by the grabber
+ ///
+ /// @param[in] device identifier of audio device
+ void setDevice(const QString& device);
+
+ ///
+ /// Set Configuration
+ ///
+ /// sets the audio grabber's configuration parameters
+ ///
+ /// @param[in] config object of configuration parameters
+ void setConfiguration(const QJsonObject& config);
+
+ ///
+ /// Reset Multiplier
+ ///
+ /// resets the calcualted audio multiplier so that it is recalculated
+ /// currently the multiplier is only reduced based on loudness.
+ ///
+ /// TODO: also calculate a low signal and reset the multiplier
+ ///
+ void resetMultiplier();
+
+ ///
+ /// Discover
+ ///
+ /// discovers audio devices in the system
+ ///
+ /// @param[in] params discover parameters
+ /// @return array of audio devices
+ virtual QJsonArray discover(const QJsonObject& params);
+
+ signals:
+ void newFrame(const Image& image);
+
+ protected:
+
+ ///
+ /// Process Audio Frame
+ ///
+ /// this functions takes in an audio buffer and emits a visual representation of the audio data
+ ///
+ /// @param[in] buffer The audio buffer to process
+ /// @param[in] length The length of audio data in the buffer
+ void processAudioFrame(int16_t* buffer, int length);
+
+ ///
+ /// Audio device id / properties map
+ ///
+ /// properties include information such as name, inputs, and etc...
+ ///
+ QMap _deviceProperties;
+
+ ///
+ /// Current device
+ ///
+ QString _device;
+
+ ///
+ /// Hot Color
+ ///
+ /// the color of the leds when the signal is high or hot
+ ///
+ QColor _hotColor;
+
+ ///
+ /// Warn value
+ ///
+ /// The maximum value of the warning color. above this threshold the signal is considered hot
+ ///
+ int _warnValue;
+
+ ///
+ /// Warn color
+ ///
+ /// the color of the leds when the signal is in between the safe and warn value threshold
+ ///
+ QColor _warnColor;
+
+ ///
+ /// Save value
+ ///
+ /// The maximum value of the safe color. above this threshold the signal enteres the warn zone.
+ /// below the signal is in the safe zone.
+ ///
+ int _safeValue;
+
+ ///
+ /// Safe color
+ ///
+ /// the color of the leds when the signal is below the safe threshold
+ ///
+ QColor _safeColor;
+
+ ///
+ /// Multiplier
+ ///
+ /// this value is used to multiply the input signal value. Some inputs may have a very low signal
+ /// and the multiplier is used to get the desired visualization.
+ ///
+ /// When the multiplier is configured to 0, the multiplier is automatically configured based off of the average
+ /// signal amplitude and tolernace.
+ ///
+ double _multiplier;
+
+ ///
+ /// Tolerance
+ ///
+ /// The tolerance is used to calculate what percentage of the top end part of the signal to ignore when
+ /// calculating the multiplier. This enables the effect to reach the hot zone with an auto configured multiplier
+ ///
+ int _tolerance;
+
+ ///
+ /// Dynamic Multiplier
+ ///
+ /// This is the current value of the automatically configured multiplier.
+ ///
+ double _dynamicMultiplier;
+
+ ///
+ /// Started
+ ///
+ /// true if the capturing session has started.
+ ///
+ bool _started;
+
+private:
+ ///
+ /// @brief free the _screen pointer
+ ///
+ void freeResources();
+};
+
+#endif // AUDIOGRABBER_H
diff --git a/include/grabber/AudioGrabberLinux.h b/include/grabber/AudioGrabberLinux.h
new file mode 100644
index 00000000..0f19ae6c
--- /dev/null
+++ b/include/grabber/AudioGrabberLinux.h
@@ -0,0 +1,91 @@
+#ifndef AUDIOGRABBERLINUX_H
+#define AUDIOGRABBERLINUX_H
+
+#include
+#include
+#include
+
+// Hyperion-utils includes
+#include
+
+///
+/// @brief The Linux Audio capture implementation
+///
+class AudioGrabberLinux : public AudioGrabber
+{
+ public:
+
+ AudioGrabberLinux();
+ ~AudioGrabberLinux() override;
+
+ ///
+ /// Process audio buffer
+ ///
+ void processAudioBuffer(snd_pcm_sframes_t frames);
+
+ ///
+ /// Is Running Flag
+ ///
+ std::atomic _isRunning;
+
+ ///
+ /// Current capture device
+ ///
+ snd_pcm_t * _captureDevice;
+
+ public slots:
+
+ ///
+ /// Start audio capturing session
+ ///
+ /// @returns true if successful
+ bool start() override;
+
+ ///
+ /// Stop audio capturing session
+ ///
+ void stop() override;
+
+ ///
+ /// Discovery audio devices
+ ///
+ QJsonArray discover(const QJsonObject& params) override;
+
+ private:
+ ///
+ /// Refresh audio devices
+ ///
+ void refreshDevices();
+
+ ///
+ /// Configure current audio capture interface
+ ///
+ bool configureCaptureInterface();
+
+ ///
+ /// Get device name from path
+ ///
+ QString getDeviceName(const QString& devicePath) const;
+
+ ///
+ /// Current sample rate
+ ///
+ unsigned int _sampleRate;
+
+ ///
+ /// Audio capture thread
+ ///
+ pthread_t _audioThread;
+
+ ///
+ /// ALSA device configuration parameters
+ ///
+ snd_pcm_hw_params_t * _captureDeviceConfig;
+};
+
+///
+/// Audio processing thread function
+///
+static void* AudioThreadRunner(void* params);
+
+#endif // AUDIOGRABBERLINUX_H
diff --git a/include/grabber/AudioGrabberWindows.h b/include/grabber/AudioGrabberWindows.h
new file mode 100644
index 00000000..747212c2
--- /dev/null
+++ b/include/grabber/AudioGrabberWindows.h
@@ -0,0 +1,81 @@
+#ifndef AUDIOGRABBERWINDOWS_H
+#define AUDIOGRABBERWINDOWS_H
+
+// Hyperion-utils includes
+#include
+#include
+
+///
+/// @brief The Windows Audio capture implementation
+///
+class AudioGrabberWindows : public AudioGrabber
+{
+ public:
+
+ AudioGrabberWindows();
+ ~AudioGrabberWindows() override;
+
+ public slots:
+ bool start() override;
+ void stop() override;
+ QJsonArray discover(const QJsonObject& params) override;
+
+ private:
+ void refreshDevices();
+ bool configureCaptureInterface();
+ QString getDeviceName(const QString& devicePath) const;
+
+ void processAudioBuffer();
+
+ LPDIRECTSOUNDCAPTURE8 recordingDevice;
+ LPDIRECTSOUNDCAPTUREBUFFER8 recordingBuffer;
+
+ HANDLE audioThread;
+ DWORD bufferCapturePosition;
+ DWORD bufferCaptureSize;
+ DWORD notificationSize;
+
+ static DWORD WINAPI AudioThreadRunner(LPVOID param);
+ HANDLE notificationEvent;
+ std::atomic isRunning{ false };
+
+static BOOL CALLBACK DirectSoundEnumProcessor(LPGUID deviceIdGuid, LPCTSTR deviceDescStr,
+ LPCTSTR deviceModelStr, LPVOID context)
+{
+ // Skip undefined audio devices
+ if (deviceIdGuid == NULL)
+ return TRUE;
+
+ QMap* devices = (QMap*)context;
+
+ AudioGrabber::DeviceProperties device;
+
+ // Process Device ID
+ LPOLESTR deviceIdStr;
+ HRESULT res = StringFromCLSID(*deviceIdGuid, &deviceIdStr);
+ if (FAILED(res))
+ {
+ Error(Logger::getInstance("AUDIOGRABBER"), "Failed to get CLSID-string for %s with error: 0x%08x: %s", deviceDescStr, res, std::system_category().message(res).c_str());
+ return FALSE;
+ }
+
+ QString deviceId = QString::fromWCharArray(deviceIdStr);
+
+ CoTaskMemFree(deviceIdStr);
+
+ // Process Device Information
+ QString deviceName = QString::fromLocal8Bit(deviceDescStr);
+
+ Debug(Logger::getInstance("AUDIOGRABBER"), "Found Audio Device: %s", deviceDescStr);
+
+ device.id = deviceId;
+ device.name = deviceName;
+
+ devices->insert(deviceId, device);
+
+ return TRUE;
+}
+
+};
+
+#endif // AUDIOGRABBERWINDOWS_H
diff --git a/include/grabber/AudioWrapper.h b/include/grabber/AudioWrapper.h
new file mode 100644
index 00000000..9e13c933
--- /dev/null
+++ b/include/grabber/AudioWrapper.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include
+
+#ifdef WIN32
+ #include
+#endif
+
+#ifdef __linux__
+ #include
+#endif
+
+///
+/// Audio Grabber wrapper
+///
+class AudioWrapper : public GrabberWrapper
+{
+ public:
+
+ // The AudioWrapper has no params...
+
+ ///
+ /// Constructs the Audio grabber with a specified grab size and update rate.
+ ///
+ /// @param[in] device Audio Device Identifier
+ /// @param[in] updateRate_Hz The audio grab rate [Hz]
+ ///
+ AudioWrapper();
+
+ ///
+ /// Destructor of this Audio grabber. Releases any claimed resources.
+ ///
+ ~AudioWrapper() override;
+
+ ///
+ /// Settings update handler
+ ///
+ void handleSettingsUpdate(settings::type type, const QJsonDocument& config) override;
+
+ public slots:
+ ///
+ /// Performs a single frame grab and computes the led-colors
+ ///
+ void action() override;
+
+ ///
+ /// Start audio capturing session
+ ///
+ /// @returns true if successful
+ bool start() override;
+
+ ///
+ /// Stop audio capturing session
+ ///
+ void stop() override;
+
+ private:
+ void newFrame(const Image& image);
+
+ /// The actual grabber
+#ifdef WIN32
+ AudioGrabberWindows _grabber;
+#endif
+
+#ifdef __linux__
+ AudioGrabberLinux _grabber;
+#endif
+
+};
diff --git a/include/grabber/GrabberType.h b/include/grabber/GrabberType.h
index c5e1de8a..a2ebd431 100644
--- a/include/grabber/GrabberType.h
+++ b/include/grabber/GrabberType.h
@@ -4,12 +4,14 @@
enum class GrabberType {
SCREEN,
VIDEO,
+ AUDIO,
};
enum class GrabberTypeFilter {
ALL,
SCREEN,
VIDEO,
+ AUDIO,
};
#endif // GRABBERTYPE_H
diff --git a/include/hyperion/CaptureCont.h b/include/hyperion/CaptureCont.h
index 93e63eab..c1eca98e 100644
--- a/include/hyperion/CaptureCont.h
+++ b/include/hyperion/CaptureCont.h
@@ -20,6 +20,7 @@ public:
void setSystemCaptureEnable(bool enable);
void setV4LCaptureEnable(bool enable);
+ void setAudioCaptureEnable(bool enable);
private slots:
///
@@ -48,11 +49,22 @@ private slots:
///
void handleV4lImage(const QString& name, const Image & image);
+ ///
+ /// @brief forward audio image
+ /// @param image The image
+ ///
+ void handleAudioImage(const QString& name, const Image& image);
+
///
/// @brief Is called from _v4lInactiveTimer to set source after specific time to inactive
///
void setV4lInactive();
+ ///
+ /// @brief Is called from _audioInactiveTimer to set source after specific time to inactive
+ ///
+ void setAudioInactive();
+
///
/// @brief Is called from _systemInactiveTimer to set source after specific time to inactive
///
@@ -73,4 +85,10 @@ private:
quint8 _v4lCaptPrio;
QString _v4lCaptName;
QTimer* _v4lInactiveTimer;
+
+ /// Reflect state of audio capture and prio
+ bool _audioCaptEnabled;
+ quint8 _audioCaptPrio;
+ QString _audioCaptName;
+ QTimer* _audioInactiveTimer;
};
diff --git a/include/hyperion/GrabberWrapper.h b/include/hyperion/GrabberWrapper.h
index fd58449c..8edf6ab0 100644
--- a/include/hyperion/GrabberWrapper.h
+++ b/include/hyperion/GrabberWrapper.h
@@ -43,8 +43,10 @@ public:
static QMap GRABBER_SYS_CLIENTS;
static QMap GRABBER_V4L_CLIENTS;
+ static QMap GRABBER_AUDIO_CLIENTS;
static bool GLOBAL_GRABBER_SYS_ENABLE;
static bool GLOBAL_GRABBER_V4L_ENABLE;
+ static bool GLOBAL_GRABBER_AUDIO_ENABLE;
///
/// Starts the grabber which produces led values with the specified update rate
@@ -78,6 +80,8 @@ public:
void setSysGrabberState(bool sysGrabberState){ GLOBAL_GRABBER_SYS_ENABLE = sysGrabberState; }
bool getV4lGrabberState() const { return GLOBAL_GRABBER_V4L_ENABLE; }
void setV4lGrabberState(bool v4lGrabberState){ GLOBAL_GRABBER_V4L_ENABLE = v4lGrabberState; }
+ bool getAudioGrabberState() const { return GLOBAL_GRABBER_AUDIO_ENABLE; }
+ void setAudioGrabberState(bool audioGrabberState) { GLOBAL_GRABBER_AUDIO_ENABLE = audioGrabberState; }
static QStringList availableGrabbers(GrabberTypeFilter type = GrabberTypeFilter::ALL);
diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h
index 34351502..3a442f40 100644
--- a/include/hyperion/Hyperion.h
+++ b/include/hyperion/Hyperion.h
@@ -454,6 +454,9 @@ signals:
/// Signal which is emitted, when a new V4l proto image should be forwarded
void forwardV4lProtoMessage(const QString&, const Image&);
+ /// Signal which is emitted, when a new Audio proto image should be forwarded
+ void forwardAudioProtoMessage(const QString&, const Image&);
+
#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
/// Signal which is emitted, when a new Flat-/Proto- Buffer image should be forwarded
void forwardBufferMessage(const QString&, const Image&);
diff --git a/include/utils/Components.h b/include/utils/Components.h
index ac5c95ed..841604a9 100644
--- a/include/utils/Components.h
+++ b/include/utils/Components.h
@@ -23,6 +23,7 @@ enum Components
#endif
COMP_GRABBER,
COMP_V4L,
+ COMP_AUDIO,
COMP_COLOR,
COMP_IMAGE,
COMP_EFFECT,
@@ -50,6 +51,7 @@ inline const char* componentToString(Components c)
#endif
case COMP_GRABBER: return "Framegrabber";
case COMP_V4L: return "V4L capture device";
+ case COMP_AUDIO: return "Audio capture device";
case COMP_COLOR: return "Solid color";
case COMP_EFFECT: return "Effect";
case COMP_IMAGE: return "Image";
@@ -79,6 +81,7 @@ inline const char* componentToIdString(Components c)
#endif
case COMP_GRABBER: return "GRABBER";
case COMP_V4L: return "V4L";
+ case COMP_AUDIO: return "AUDIO";
case COMP_COLOR: return "COLOR";
case COMP_EFFECT: return "EFFECT";
case COMP_IMAGE: return "IMAGE";
@@ -107,6 +110,7 @@ inline Components stringToComponent(const QString& component)
#endif
if (cmp == "GRABBER") return COMP_GRABBER;
if (cmp == "V4L") return COMP_V4L;
+ if (cmp == "AUDIO") return COMP_AUDIO;
if (cmp == "COLOR") return COMP_COLOR;
if (cmp == "EFFECT") return COMP_EFFECT;
if (cmp == "IMAGE") return COMP_IMAGE;
diff --git a/include/utils/GlobalSignals.h b/include/utils/GlobalSignals.h
index 8819d8ce..8fbdba29 100644
--- a/include/utils/GlobalSignals.h
+++ b/include/utils/GlobalSignals.h
@@ -56,6 +56,13 @@ signals:
void setBufferImage(const QString& name, const Image& image);
#endif
+ ///
+ /// @brief PIPE audioCapture images from audioCapture over HyperionDaemon to Hyperion class
+ /// @param name The name of the audio capture (path) that is currently active
+ /// @param image The prepared image
+ ///
+ void setAudioImage(const QString& name, const Image& image);
+
///
/// @brief PIPE the register command for a new global input over HyperionDaemon to Hyperion class
/// @param[in] priority The priority of the channel
diff --git a/include/utils/settings.h b/include/utils/settings.h
index 175a2df1..dab90204 100644
--- a/include/utils/settings.h
+++ b/include/utils/settings.h
@@ -19,6 +19,7 @@ namespace settings {
SYSTEMCAPTURE,
GENERAL,
V4L2,
+ AUDIO,
JSONSERVER,
LEDCONFIG,
LEDS,
@@ -52,6 +53,7 @@ namespace settings {
case SYSTEMCAPTURE: return "framegrabber";
case GENERAL: return "general";
case V4L2: return "grabberV4L2";
+ case AUDIO: return "grabberAudio";
case JSONSERVER: return "jsonServer";
case LEDCONFIG: return "ledConfig";
case LEDS: return "leds";
@@ -84,6 +86,7 @@ namespace settings {
else if (type == "framegrabber") return SYSTEMCAPTURE;
else if (type == "general") return GENERAL;
else if (type == "grabberV4L2") return V4L2;
+ else if (type == "grabberAudio") return AUDIO;
else if (type == "jsonServer") return JSONSERVER;
else if (type == "ledConfig") return LEDCONFIG;
else if (type == "leds") return LEDS;
diff --git a/libsrc/api/JSONRPC_schema/schema-componentstate.json b/libsrc/api/JSONRPC_schema/schema-componentstate.json
index 08cd1912..f46324dc 100644
--- a/libsrc/api/JSONRPC_schema/schema-componentstate.json
+++ b/libsrc/api/JSONRPC_schema/schema-componentstate.json
@@ -21,7 +21,7 @@
"component":
{
"type" : "string",
- "enum" : ["ALL", "SMOOTHING", "BLACKBORDER", "FORWARDER", "BOBLIGHTSERVER", "GRABBER", "V4L", "LEDDEVICE"],
+ "enum" : ["ALL", "SMOOTHING", "BLACKBORDER", "FORWARDER", "BOBLIGHTSERVER", "GRABBER", "V4L", "AUDIO", "LEDDEVICE"],
"required": true
},
"state":
diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp
index e4418d54..212dc14b 100644
--- a/libsrc/api/JsonAPI.cpp
+++ b/libsrc/api/JsonAPI.cpp
@@ -29,6 +29,18 @@
#include
#endif
+#if defined(ENABLE_AUDIO)
+ #include
+
+ #ifdef WIN32
+ #include
+ #endif
+
+ #ifdef __linux__
+ #include
+ #endif
+#endif
+
#if defined(ENABLE_X11)
#include
#endif
@@ -554,6 +566,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString
info["ledDevices"] = ledDevices;
QJsonObject grabbers;
+ // SCREEN
QJsonObject screenGrabbers;
if (GrabberWrapper::getInstance() != nullptr)
{
@@ -573,6 +586,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString
}
screenGrabbers["available"] = availableScreenGrabbers;
+ // VIDEO
QJsonObject videoGrabbers;
if (GrabberWrapper::getInstance() != nullptr)
{
@@ -592,8 +606,31 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString
}
videoGrabbers["available"] = availableVideoGrabbers;
+ // AUDIO
+ QJsonObject audioGrabbers;
+ if (GrabberWrapper::getInstance() != nullptr)
+ {
+ QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex(), GrabberTypeFilter::AUDIO);
+
+ QJsonArray activeGrabberNames;
+ for (auto grabberName : activeGrabbers)
+ {
+ activeGrabberNames.append(grabberName);
+ }
+
+ audioGrabbers["active"] = activeGrabberNames;
+ }
+ QJsonArray availableAudioGrabbers;
+ for (auto grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::AUDIO))
+ {
+ availableAudioGrabbers.append(grabber);
+ }
+ audioGrabbers["available"] = availableAudioGrabbers;
+
grabbers.insert("screen", screenGrabbers);
grabbers.insert("video", videoGrabbers);
+ grabbers.insert("audio", audioGrabbers);
+
info["grabbers"] = grabbers;
info["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode()));
@@ -1607,6 +1644,7 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString
QJsonObject inputSourcesDiscovered;
inputSourcesDiscovered.insert("sourceType", sourceType);
QJsonArray videoInputs;
+ QJsonArray audioInputs;
#if defined(ENABLE_V4L2) || defined(ENABLE_MF)
@@ -1623,6 +1661,24 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString
}
else
#endif
+
+#if defined(ENABLE_AUDIO)
+ if (sourceType == "audio")
+ {
+ AudioGrabber* grabber;
+#ifdef WIN32
+ grabber = new AudioGrabberWindows();
+#endif
+
+#ifdef __linux__
+ grabber = new AudioGrabberLinux();
+#endif
+ QJsonObject params;
+ audioInputs = grabber->discover(params);
+ delete grabber;
+ }
+ else
+#endif
{
DebugIf(verbose, _log, "sourceType: [%s]", QSTRING_CSTR(sourceType));
@@ -1719,6 +1775,7 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString
}
inputSourcesDiscovered["video_sources"] = videoInputs;
+ inputSourcesDiscovered["audio_sources"] = audioInputs;
DebugIf(verbose, _log, "response: [%s]", QString(QJsonDocument(inputSourcesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData());
diff --git a/libsrc/forwarder/MessageForwarder.cpp b/libsrc/forwarder/MessageForwarder.cpp
index e9dc40f9..b795cbe8 100644
--- a/libsrc/forwarder/MessageForwarder.cpp
+++ b/libsrc/forwarder/MessageForwarder.cpp
@@ -115,6 +115,9 @@ void MessageForwarder::enableTargets(bool enable, const QJsonObject& config)
case hyperion::COMP_V4L:
connect(_hyperion, &Hyperion::forwardV4lProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection);
break;
+ case hyperion::COMP_AUDIO:
+ connect(_hyperion, &Hyperion::forwardAudioProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection);
+ break;
#if defined(ENABLE_FLATBUF_SERVER)
case hyperion::COMP_FLATBUFSERVER:
#endif
@@ -153,6 +156,7 @@ void MessageForwarder::handlePriorityChanges(int priority)
switch (activeCompId) {
case hyperion::COMP_GRABBER:
disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr);
+ disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr);
#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr);
#endif
@@ -160,11 +164,20 @@ void MessageForwarder::handlePriorityChanges(int priority)
break;
case hyperion::COMP_V4L:
disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr);
+ disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr);
#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr);
#endif
connect(_hyperion, &Hyperion::forwardV4lProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection);
break;
+ case hyperion::COMP_AUDIO:
+ disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr);
+ disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr);
+#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
+ disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr);
+#endif
+ connect(_hyperion, &Hyperion::forwardAudioProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection);
+ break;
#if defined(ENABLE_FLATBUF_SERVER)
case hyperion::COMP_FLATBUFSERVER:
#endif
@@ -172,6 +185,7 @@ void MessageForwarder::handlePriorityChanges(int priority)
case hyperion::COMP_PROTOSERVER:
#endif
#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
+ disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr);
disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr);
disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr);
connect(_hyperion, &Hyperion::forwardBufferMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection);
@@ -180,6 +194,7 @@ void MessageForwarder::handlePriorityChanges(int priority)
default:
disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr);
disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr);
+ disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr);
#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr);
#endif
@@ -373,6 +388,7 @@ void MessageForwarder::stopFlatbufferTargets()
{
disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr);
disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr);
+ disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr);
#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER)
disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr);
#endif
diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt
index 5c0feea1..c729ad0a 100644
--- a/libsrc/grabber/CMakeLists.txt
+++ b/libsrc/grabber/CMakeLists.txt
@@ -33,3 +33,7 @@ endif(ENABLE_QT)
if (ENABLE_DX)
add_subdirectory(directx)
endif(ENABLE_DX)
+
+if (ENABLE_AUDIO)
+ add_subdirectory(audio)
+endif()
diff --git a/libsrc/grabber/audio/AudioGrabber.cpp b/libsrc/grabber/audio/AudioGrabber.cpp
new file mode 100644
index 00000000..1e625a1e
--- /dev/null
+++ b/libsrc/grabber/audio/AudioGrabber.cpp
@@ -0,0 +1,201 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Constants
+namespace {
+ const uint16_t RESOLUTION = 255;
+}
+
+#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
+namespace QColorConstants
+{
+ const QColor Black = QColor(0xFF, 0x00, 0x00);
+ const QColor Red = QColor(0xFF, 0x00, 0x00);
+ const QColor Green = QColor(0x00, 0xFF, 0x00);
+ const QColor Blue = QColor(0x00, 0x00, 0xFF);
+ const QColor Yellow = QColor(0xFF, 0xFF, 0x00);
+}
+#endif
+ //End of constants
+
+AudioGrabber::AudioGrabber()
+ : Grabber("AudioGrabber")
+ , _deviceProperties()
+ , _device("none")
+ , _hotColor(QColorConstants::Red)
+ , _warnValue(80)
+ , _warnColor(QColorConstants::Yellow)
+ , _safeValue(45)
+ , _safeColor(QColorConstants::Green)
+ , _multiplier(0)
+ , _tolerance(20)
+ , _dynamicMultiplier(INT16_MAX)
+ , _started(false)
+{
+}
+
+AudioGrabber::~AudioGrabber()
+{
+ freeResources();
+}
+
+void AudioGrabber::freeResources()
+{
+}
+
+void AudioGrabber::setDevice(const QString& device)
+{
+ _device = device;
+
+ if (_started)
+ {
+ this->stop();
+ this->start();
+ }
+}
+
+void AudioGrabber::setConfiguration(const QJsonObject& config)
+{
+ QJsonArray hotColorArray = config["hotColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(255), QVariant(0), QVariant(0) })));
+ QJsonArray warnColorArray = config["warnColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(255), QVariant(255), QVariant(0) })));
+ QJsonArray safeColorArray = config["safeColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(0), QVariant(255), QVariant(0) })));
+
+ _hotColor = QColor(hotColorArray.at(0).toInt(), hotColorArray.at(1).toInt(), hotColorArray.at(2).toInt());
+ _warnColor = QColor(warnColorArray.at(0).toInt(), warnColorArray.at(1).toInt(), warnColorArray.at(2).toInt());
+ _safeColor = QColor(safeColorArray.at(0).toInt(), safeColorArray.at(1).toInt(), safeColorArray.at(2).toInt());
+
+ _warnValue = config["warnValue"].toInt(80);
+ _safeValue = config["safeValue"].toInt(45);
+ _multiplier = config["multiplier"].toDouble(0);
+ _tolerance = config["tolerance"].toInt(20);
+}
+
+void AudioGrabber::resetMultiplier()
+{
+ _dynamicMultiplier = INT16_MAX;
+}
+
+void AudioGrabber::processAudioFrame(int16_t* buffer, int length)
+{
+ // Apply Visualizer and Construct Image
+
+ // TODO: Pass Audio Frame to python and let the script calculate the image.
+
+ // TODO: Support Stereo capture with different meters per side
+
+ // Default VUMeter - Later Make this pluggable for different audio effects
+
+ double averageAmplitude = 0;
+ // Calculate the the average amplitude value in the buffer
+ for (int i = 0; i < length; i++)
+ {
+ averageAmplitude += fabs(buffer[i]) / length;
+ }
+
+ double * currentMultiplier;
+
+ if (_multiplier < std::numeric_limits::epsilon())
+ {
+ // Dynamically calculate multiplier.
+ const double pendingMultiplier = INT16_MAX / fmax(1.0, averageAmplitude + ((_tolerance / 100.0) * averageAmplitude));
+
+ if (pendingMultiplier < _dynamicMultiplier)
+ _dynamicMultiplier = pendingMultiplier;
+
+ currentMultiplier = &_dynamicMultiplier;
+ }
+ else
+ {
+ // User defined multiplier
+ currentMultiplier = &_multiplier;
+ }
+
+ // Apply multiplier to average amplitude
+ const double result = averageAmplitude * (*currentMultiplier);
+
+ // Calculate the average percentage
+ const double percentage = fmin(result / INT16_MAX, 1);
+
+ // Calculate the value
+ const int value = static_cast(ceil(percentage * RESOLUTION));
+
+ // Draw Image
+ QImage image(1, RESOLUTION, QImage::Format_RGB888);
+ image.fill(QColorConstants::Black);
+
+ int safePixelValue = static_cast(round(( _safeValue / 100.0) * RESOLUTION));
+ int warnPixelValue = static_cast(round(( _warnValue / 100.0) * RESOLUTION));
+
+ for (int i = 0; i < RESOLUTION; i++)
+ {
+ QColor color = QColorConstants::Black;
+ int position = RESOLUTION - i;
+
+ if (position < safePixelValue)
+ {
+ color = _safeColor;
+ }
+ else if (position < warnPixelValue)
+ {
+ color = _warnColor;
+ }
+ else
+ {
+ color = _hotColor;
+ }
+
+ if (position < value)
+ {
+ image.setPixelColor(0, i, color);
+ }
+ else
+ {
+ image.setPixelColor(0, i, QColorConstants::Black);
+ }
+ }
+
+ // Convert to Image
+ Image finalImage (static_cast(image.width()), static_cast(image.height()));
+ for (int y = 0; y < image.height(); y++)
+ {
+ memcpy((unsigned char*)finalImage.memptr() + y * image.width() * 3, static_cast(image.scanLine(y)), image.width() * 3);
+ }
+
+ emit newFrame(finalImage);
+}
+
+Logger* AudioGrabber::getLog()
+{
+ return _log;
+}
+
+bool AudioGrabber::start()
+{
+ resetMultiplier();
+
+ _started = true;
+
+ return true;
+}
+
+void AudioGrabber::stop()
+{
+ _started = false;
+}
+
+void AudioGrabber::restart()
+{
+ stop();
+ start();
+}
+
+QJsonArray AudioGrabber::discover(const QJsonObject& /*params*/)
+{
+ QJsonArray result; // Return empty result
+ return result;
+}
diff --git a/libsrc/grabber/audio/AudioGrabberLinux.cpp b/libsrc/grabber/audio/AudioGrabberLinux.cpp
new file mode 100644
index 00000000..8938e043
--- /dev/null
+++ b/libsrc/grabber/audio/AudioGrabberLinux.cpp
@@ -0,0 +1,317 @@
+#include
+
+#include
+
+#include
+#include
+
+typedef void* (*THREADFUNCPTR)(void*);
+
+AudioGrabberLinux::AudioGrabberLinux()
+ : AudioGrabber()
+ , _isRunning{ false }
+ , _captureDevice {nullptr}
+ , _sampleRate(44100)
+{
+}
+
+AudioGrabberLinux::~AudioGrabberLinux()
+{
+ this->stop();
+}
+
+void AudioGrabberLinux::refreshDevices()
+{
+ Debug(_log, "Enumerating Audio Input Devices");
+
+ _deviceProperties.clear();
+
+ snd_ctl_t* deviceHandle;
+ int soundCard {-1};
+ int error {-1};
+ int cardInput {-1};
+
+ snd_ctl_card_info_t* cardInfo;
+ snd_pcm_info_t* deviceInfo;
+
+ snd_ctl_card_info_alloca(&cardInfo);
+ snd_pcm_info_alloca(&deviceInfo);
+
+ while (snd_card_next(&soundCard) > -1)
+ {
+ if (soundCard < 0)
+ {
+ break;
+ }
+
+ char cardId[32];
+ sprintf(cardId, "hw:%d", soundCard);
+
+ if ((error = snd_ctl_open(&deviceHandle, cardId, SND_CTL_NONBLOCK)) < 0)
+ {
+ Error(_log, "Erorr opening device: (%i): %s", soundCard, snd_strerror(error));
+ continue;
+ }
+
+ if ((error = snd_ctl_card_info(deviceHandle, cardInfo)) < 0)
+ {
+ Error(_log, "Erorr getting hardware info: (%i): %s", soundCard, snd_strerror(error));
+ snd_ctl_close(deviceHandle);
+ continue;
+ }
+
+ cardInput = -1;
+
+ while (true)
+ {
+ if (snd_ctl_pcm_next_device(deviceHandle, &cardInput) < 0)
+ Error(_log, "Error selecting device input");
+
+ if (cardInput < 0)
+ break;
+
+ snd_pcm_info_set_device(deviceInfo, static_cast(cardInput));
+ snd_pcm_info_set_subdevice(deviceInfo, 0);
+ snd_pcm_info_set_stream(deviceInfo, SND_PCM_STREAM_CAPTURE);
+
+ if ((error = snd_ctl_pcm_info(deviceHandle, deviceInfo)) < 0)
+ {
+ if (error != -ENOENT)
+ Error(_log, "Digital Audio Info: (%i): %s", soundCard, snd_strerror(error));
+
+ continue;
+ }
+
+ AudioGrabber::DeviceProperties device;
+
+ device.id = QString("hw:%1,%2").arg(snd_pcm_info_get_card(deviceInfo)).arg(snd_pcm_info_get_device(deviceInfo));
+ device.name = QString("%1: %2").arg(snd_ctl_card_info_get_name(cardInfo),snd_pcm_info_get_name(deviceInfo));
+
+ Debug(_log, "Found sound card (%s): %s", QSTRING_CSTR(device.id), QSTRING_CSTR(device.name));
+
+ _deviceProperties.insert(device.id, device);
+ }
+
+ snd_ctl_close(deviceHandle);
+ }
+}
+
+bool AudioGrabberLinux::configureCaptureInterface()
+{
+ int error = -1;
+ QString name = (_device.isEmpty() || _device == "auto") ? "default" : (_device);
+
+ if ((error = snd_pcm_open(&_captureDevice, QSTRING_CSTR(name) , SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)) < 0)
+ {
+ Error(_log, "Failed to open audio device: %s, - %s", QSTRING_CSTR(_device), snd_strerror(error));
+ return false;
+ }
+
+ if ((error = snd_pcm_hw_params_malloc(&_captureDeviceConfig)) < 0)
+ {
+ Error(_log, "Failed to create hardware parameters: %s", snd_strerror(error));
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ if ((error = snd_pcm_hw_params_any(_captureDevice, _captureDeviceConfig)) < 0)
+ {
+ Error(_log, "Failed to initialize hardware parameters: %s", snd_strerror(error));
+ snd_pcm_hw_params_free(_captureDeviceConfig);
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ if ((error = snd_pcm_hw_params_set_access(_captureDevice, _captureDeviceConfig, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
+ {
+ Error(_log, "Failed to configure interleaved mode: %s", snd_strerror(error));
+ snd_pcm_hw_params_free(_captureDeviceConfig);
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ if ((error = snd_pcm_hw_params_set_format(_captureDevice, _captureDeviceConfig, SND_PCM_FORMAT_S16_LE)) < 0)
+ {
+ Error(_log, "Failed to configure capture format: %s", snd_strerror(error));
+ snd_pcm_hw_params_free(_captureDeviceConfig);
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ if ((error = snd_pcm_hw_params_set_rate_near(_captureDevice, _captureDeviceConfig, &_sampleRate, nullptr)) < 0)
+ {
+ Error(_log, "Failed to configure sample rate: %s", snd_strerror(error));
+ snd_pcm_hw_params_free(_captureDeviceConfig);
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ if ((error = snd_pcm_hw_params(_captureDevice, _captureDeviceConfig)) < 0)
+ {
+ Error(_log, "Failed to configure hardware parameters: %s", snd_strerror(error));
+ snd_pcm_hw_params_free(_captureDeviceConfig);
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ snd_pcm_hw_params_free(_captureDeviceConfig);
+
+ if ((error = snd_pcm_prepare(_captureDevice)) < 0)
+ {
+ Error(_log, "Failed to prepare audio interface: %s", snd_strerror(error));
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ if ((error = snd_pcm_start(_captureDevice)) < 0)
+ {
+ Error(_log, "Failed to start audio interface: %s", snd_strerror(error));
+ snd_pcm_close(_captureDevice);
+ return false;
+ }
+
+ return true;
+}
+
+bool AudioGrabberLinux::start()
+{
+ if (!_isEnabled)
+ return false;
+
+ if (_isRunning.load(std::memory_order_acquire))
+ return true;
+
+ Debug(_log, "Start Audio With %s", QSTRING_CSTR(getDeviceName(_device)));
+
+ if (!configureCaptureInterface())
+ return false;
+
+ _isRunning.store(true, std::memory_order_release);
+
+ pthread_attr_t threadAttributes;
+ int threadPriority = 1;
+
+ sched_param schedulerParameter;
+ schedulerParameter.sched_priority = threadPriority;
+
+ if (pthread_attr_init(&threadAttributes) != 0)
+ {
+ Debug(_log, "Failed to create thread attributes");
+ stop();
+ return false;
+ }
+
+ if (pthread_create(&_audioThread, &threadAttributes, static_cast(&AudioThreadRunner), static_cast(this)) != 0)
+ {
+ Debug(_log, "Failed to create audio capture thread");
+ stop();
+ return false;
+ }
+
+ AudioGrabber::start();
+
+ return true;
+}
+
+void AudioGrabberLinux::stop()
+{
+ if (!_isRunning.load(std::memory_order_acquire))
+ return;
+
+ Debug(_log, "Stopping Audio Interface");
+
+ _isRunning.store(false, std::memory_order_release);
+
+ if (_audioThread != 0) {
+ pthread_join(_audioThread, NULL);
+ }
+
+ snd_pcm_close(_captureDevice);
+
+ AudioGrabber::stop();
+}
+
+void AudioGrabberLinux::processAudioBuffer(snd_pcm_sframes_t frames)
+{
+ if (!_isRunning.load(std::memory_order_acquire))
+ return;
+
+ ssize_t bytes = snd_pcm_frames_to_bytes(_captureDevice, frames);
+
+ int16_t * buffer = static_cast(calloc(static_cast(bytes / 2), sizeof(int16_t)));
+
+ if (frames == 0)
+ {
+ buffer[0] = 0;
+ processAudioFrame(buffer, 1);
+ }
+ else
+ {
+ snd_pcm_sframes_t framesRead = snd_pcm_readi(_captureDevice, buffer, static_cast(frames));
+
+ if (framesRead < frames)
+ {
+ Error(_log, "Error reading audio. Got %d frames instead of %d", framesRead, frames);
+ }
+ else
+ {
+ processAudioFrame(buffer, static_cast(snd_pcm_frames_to_bytes(_captureDevice, framesRead)) / 2);
+ }
+ }
+
+ free(buffer);
+}
+
+QJsonArray AudioGrabberLinux::discover(const QJsonObject& /*params*/)
+{
+ refreshDevices();
+
+ QJsonArray devices;
+
+ for (auto deviceIterator = _deviceProperties.begin(); deviceIterator != _deviceProperties.end(); ++deviceIterator)
+ {
+ // Device
+ QJsonObject device;
+ QJsonArray deviceInputs;
+
+ device["device"] = deviceIterator.key();
+ device["device_name"] = deviceIterator.value().name;
+ device["type"] = "audio";
+
+ devices.append(device);
+ }
+
+ return devices;
+}
+
+QString AudioGrabberLinux::getDeviceName(const QString& devicePath) const
+{
+ if (devicePath.isEmpty() || devicePath == "auto")
+ {
+ return "Default Audio Device";
+ }
+
+ return _deviceProperties.value(devicePath).name;
+}
+
+static void * AudioThreadRunner(void* params)
+{
+ AudioGrabberLinux* This = static_cast(params);
+
+ Debug(This->getLog(), "Audio Thread Started");
+
+ snd_pcm_sframes_t framesAvailable = 0;
+
+ while (This->_isRunning.load(std::memory_order_acquire))
+ {
+ snd_pcm_wait(This->_captureDevice, 1000);
+
+ if ((framesAvailable = snd_pcm_avail(This->_captureDevice)) > 0)
+ This->processAudioBuffer(framesAvailable);
+
+ sched_yield();
+ }
+
+ Debug(This->getLog(), "Audio Thread Shutting Down");
+ return nullptr;
+}
diff --git a/libsrc/grabber/audio/AudioGrabberWindows.cpp b/libsrc/grabber/audio/AudioGrabberWindows.cpp
new file mode 100644
index 00000000..07837bd1
--- /dev/null
+++ b/libsrc/grabber/audio/AudioGrabberWindows.cpp
@@ -0,0 +1,354 @@
+#include
+#include
+#include
+#include
+
+#pragma comment(lib,"dsound.lib")
+#pragma comment(lib, "dxguid.lib")
+
+// Constants
+namespace {
+ const int AUDIO_NOTIFICATION_COUNT{ 4 };
+} //End of constants
+
+AudioGrabberWindows::AudioGrabberWindows() : AudioGrabber()
+{
+}
+
+AudioGrabberWindows::~AudioGrabberWindows()
+{
+ this->stop();
+}
+
+void AudioGrabberWindows::refreshDevices()
+{
+ Debug(_log, "Refreshing Audio Devices");
+
+ _deviceProperties.clear();
+
+ // Enumerate Devices
+ if (FAILED(DirectSoundCaptureEnumerate(DirectSoundEnumProcessor, (VOID*)&_deviceProperties)))
+ {
+ Error(_log, "Failed to enumerate audio devices.");
+ }
+}
+
+bool AudioGrabberWindows::configureCaptureInterface()
+{
+ CLSID deviceId {};
+
+ if (!this->_device.isEmpty() && this->_device != "auto")
+ {
+ LPCOLESTR clsid = reinterpret_cast(_device.utf16());
+ HRESULT res = CLSIDFromString(clsid, &deviceId);
+ if (FAILED(res))
+ {
+ Error(_log, "Failed to get CLSID for '%s' with error: 0x%08x: %s", QSTRING_CSTR(_device), res, std::system_category().message(res).c_str());
+ return false;
+ }
+ }
+
+ // Create Capture Device
+ HRESULT res = DirectSoundCaptureCreate8(&deviceId, &recordingDevice, NULL);
+ if (FAILED(res))
+ {
+ Error(_log, "Failed to create capture device: '%s' with error: 0x%08x: %s", QSTRING_CSTR(_device), res, std::system_category().message(res).c_str());
+ return false;
+ }
+
+ // Define Audio Format & Create Buffer
+ WAVEFORMATEX audioFormat { WAVE_FORMAT_PCM, 1, 44100, 88200, 2, 16, 0 };
+ // wFormatTag, nChannels, nSamplesPerSec, mAvgBytesPerSec,
+ // nBlockAlign, wBitsPerSample, cbSize
+
+ notificationSize = max(1024, audioFormat.nAvgBytesPerSec / 8);
+ notificationSize -= notificationSize % audioFormat.nBlockAlign;
+
+ bufferCaptureSize = notificationSize * AUDIO_NOTIFICATION_COUNT;
+
+ DSCBUFFERDESC bufferDesc;
+ bufferDesc.dwSize = sizeof(DSCBUFFERDESC);
+ bufferDesc.dwFlags = 0;
+ bufferDesc.dwBufferBytes = bufferCaptureSize;
+ bufferDesc.dwReserved = 0;
+ bufferDesc.lpwfxFormat = &audioFormat;
+ bufferDesc.dwFXCount = 0;
+ bufferDesc.lpDSCFXDesc = NULL;
+
+ // Create Capture Device's Buffer
+ LPDIRECTSOUNDCAPTUREBUFFER preBuffer;
+ if (FAILED(recordingDevice->CreateCaptureBuffer(&bufferDesc, &preBuffer, NULL)))
+ {
+ Error(_log, "Failed to create capture buffer: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ recordingDevice->Release();
+ return false;
+ }
+
+ bufferCapturePosition = 0;
+
+ // Query Capture8 Buffer
+ if (FAILED(preBuffer->QueryInterface(IID_IDirectSoundCaptureBuffer8, (LPVOID*)&recordingBuffer)))
+ {
+ Error(_log, "Failed to retrieve recording buffer");
+ preBuffer->Release();
+ return false;
+ }
+
+ preBuffer->Release();
+
+ // Create Notifications
+ LPDIRECTSOUNDNOTIFY8 notify;
+
+ if (FAILED(recordingBuffer->QueryInterface(IID_IDirectSoundNotify8, (LPVOID *) ¬ify)))
+ {
+ Error(_log, "Failed to configure buffer notifications: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ recordingDevice->Release();
+ recordingBuffer->Release();
+ return false;
+ }
+
+ // Create Events
+ notificationEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
+
+ if (notificationEvent == NULL)
+ {
+ Error(_log, "Failed to configure buffer notifications events: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ notify->Release();
+ recordingDevice->Release();
+ recordingBuffer->Release();
+ return false;
+ }
+
+ // Configure Notifications
+ DSBPOSITIONNOTIFY positionNotify[AUDIO_NOTIFICATION_COUNT];
+
+ for (int i = 0; i < AUDIO_NOTIFICATION_COUNT; i++)
+ {
+ positionNotify[i].dwOffset = (notificationSize * i) + notificationSize - 1;
+ positionNotify[i].hEventNotify = notificationEvent;
+ }
+
+ // Set Notifications
+ notify->SetNotificationPositions(AUDIO_NOTIFICATION_COUNT, positionNotify);
+ notify->Release();
+
+ return true;
+}
+
+bool AudioGrabberWindows::start()
+{
+ if (!_isEnabled)
+ {
+ return false;
+ }
+
+ if (this->isRunning.load(std::memory_order_acquire))
+ {
+ return true;
+ }
+
+ //Test, if configured device currently exists
+ refreshDevices();
+ if (!_deviceProperties.contains(_device))
+ {
+ _device = "auto";
+ Warning(_log, "Configured audio device is not available. Using '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ }
+
+ Info(_log, "Capture audio from %s", QSTRING_CSTR(getDeviceName(_device)));
+
+ if (!this->configureCaptureInterface())
+ {
+ return false;
+ }
+
+ if (FAILED(recordingBuffer->Start(DSCBSTART_LOOPING)))
+ {
+ Error(_log, "Failed starting audio capture from '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ return false;
+ }
+
+ this->isRunning.store(true, std::memory_order_release);
+ DWORD threadId;
+
+ this->audioThread = CreateThread(
+ NULL,
+ 16,
+ AudioThreadRunner,
+ (void *) this,
+ 0,
+ &threadId
+ );
+
+ if (this->audioThread == NULL)
+ {
+ Error(_log, "Failed to create audio capture thread");
+
+ this->stop();
+ return false;
+ }
+
+ AudioGrabber::start();
+
+ return true;
+}
+
+void AudioGrabberWindows::stop()
+{
+ if (!this->isRunning.load(std::memory_order_acquire))
+ {
+ return;
+ }
+
+ Info(_log, "Shutting down audio capture from: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+
+ this->isRunning.store(false, std::memory_order_release);
+
+ if (FAILED(recordingBuffer->Stop()))
+ {
+ Error(_log, "Audio capture failed to stop: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ }
+
+ if (FAILED(recordingBuffer->Release()))
+ {
+ Error(_log, "Failed to release recording buffer: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ }
+
+ if (FAILED(recordingDevice->Release()))
+ {
+ Error(_log, "Failed to release recording device: '%s'", QSTRING_CSTR(getDeviceName(_device)));
+ }
+
+ CloseHandle(notificationEvent);
+ CloseHandle(this->audioThread);
+
+ AudioGrabber::stop();
+}
+
+DWORD WINAPI AudioGrabberWindows::AudioThreadRunner(LPVOID param)
+{
+ AudioGrabberWindows* This = (AudioGrabberWindows*) param;
+
+ while (This->isRunning.load(std::memory_order_acquire))
+ {
+ DWORD result = WaitForMultipleObjects(1, &This->notificationEvent, true, 500);
+
+ switch (result)
+ {
+ case WAIT_OBJECT_0:
+ This->processAudioBuffer();
+ break;
+ }
+ }
+
+ Debug(This->_log, "Audio capture thread stopped.");
+
+ return 0;
+}
+
+void AudioGrabberWindows::processAudioBuffer()
+{
+ DWORD readPosition;
+ DWORD capturePosition;
+
+ // Primary segment
+ VOID* capturedAudio;
+ DWORD capturedAudioLength;
+
+ // Wrap around segment
+ VOID* capturedAudio2;
+ DWORD capturedAudio2Length;
+
+ LONG lockSize;
+
+ if (FAILED(recordingBuffer->GetCurrentPosition(&capturePosition, &readPosition)))
+ {
+ // Failed to get current position
+ Error(_log, "Failed to get buffer position.");
+ return;
+ }
+
+ lockSize = readPosition - bufferCapturePosition;
+
+ if (lockSize < 0)
+ {
+ lockSize += bufferCaptureSize;
+ }
+
+ // Block Align
+ lockSize -= (lockSize % notificationSize);
+
+ if (lockSize == 0)
+ {
+ return;
+ }
+
+ // Lock Capture Buffer
+ if (FAILED(recordingBuffer->Lock(bufferCapturePosition, lockSize, &capturedAudio, &capturedAudioLength,
+ &capturedAudio2, &capturedAudio2Length, 0)))
+ {
+ // Handle Lock Error
+ return;
+ }
+
+ bufferCapturePosition += capturedAudioLength;
+ bufferCapturePosition %= bufferCaptureSize; // Circular Buffer
+
+ int frameSize = capturedAudioLength + capturedAudio2Length;
+
+ int16_t * readBuffer = new int16_t[frameSize];
+
+ // Buffer wrapped around, read second position
+ if (capturedAudio2 != NULL)
+ {
+ bufferCapturePosition += capturedAudio2Length;
+ bufferCapturePosition %= bufferCaptureSize; // Circular Buffer
+ }
+
+ // Copy Buffer into memory
+ CopyMemory(readBuffer, capturedAudio, capturedAudioLength);
+
+ if (capturedAudio2 != NULL)
+ {
+ CopyMemory(readBuffer + capturedAudioLength, capturedAudio2, capturedAudio2Length);
+ }
+
+ // Release Buffer Lock
+ recordingBuffer->Unlock(capturedAudio, capturedAudioLength, capturedAudio2, capturedAudio2Length);
+
+ // Process Audio Frame
+ this->processAudioFrame(readBuffer, frameSize);
+
+ delete[] readBuffer;
+}
+
+QJsonArray AudioGrabberWindows::discover(const QJsonObject& params)
+{
+ refreshDevices();
+
+ QJsonArray devices;
+
+ for (auto deviceIterator = _deviceProperties.begin(); deviceIterator != _deviceProperties.end(); ++deviceIterator)
+ {
+ // Device
+ QJsonObject device;
+ QJsonArray deviceInputs;
+
+ device["device"] = deviceIterator.value().id;
+ device["device_name"] = deviceIterator.value().name;
+ device["type"] = "audio";
+
+ devices.append(device);
+ }
+
+ return devices;
+}
+
+QString AudioGrabberWindows::getDeviceName(const QString& devicePath) const
+{
+ if (devicePath.isEmpty() || devicePath == "auto")
+ {
+ return "Default Device";
+ }
+ return _deviceProperties.value(devicePath).name;
+}
diff --git a/libsrc/grabber/audio/AudioWrapper.cpp b/libsrc/grabber/audio/AudioWrapper.cpp
new file mode 100644
index 00000000..0dd624de
--- /dev/null
+++ b/libsrc/grabber/audio/AudioWrapper.cpp
@@ -0,0 +1,63 @@
+#include
+#include
+#include
+#include
+
+AudioWrapper::AudioWrapper()
+ : GrabberWrapper("AudioGrabber", &_grabber)
+ , _grabber()
+{
+ // register the image type
+ qRegisterMetaType>("Image");
+
+ connect(&_grabber, &AudioGrabber::newFrame, this, &AudioWrapper::newFrame, Qt::DirectConnection);
+}
+
+AudioWrapper::~AudioWrapper()
+{
+ stop();
+}
+
+bool AudioWrapper::start()
+{
+ return (_grabber.start() && GrabberWrapper::start());
+}
+
+void AudioWrapper::stop()
+{
+ _grabber.stop();
+ GrabberWrapper::stop();
+}
+
+void AudioWrapper::action()
+{
+ // Dummy we will push the audio images
+}
+
+void AudioWrapper::newFrame(const Image& image)
+{
+ emit systemImage(_grabberName, image);
+}
+
+void AudioWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument& config)
+{
+ if (type == settings::AUDIO)
+ {
+ const QJsonObject& obj = config.object();
+
+ // set global grabber state
+ setAudioGrabberState(obj["enable"].toBool(false));
+
+ if (getAudioGrabberState())
+ {
+ _grabber.setDevice(obj["device"].toString());
+ _grabber.setConfiguration(obj);
+
+ _grabber.restart();
+ }
+ else
+ {
+ stop();
+ }
+ }
+}
diff --git a/libsrc/grabber/audio/CMakeLists.txt b/libsrc/grabber/audio/CMakeLists.txt
new file mode 100644
index 00000000..187fa9d0
--- /dev/null
+++ b/libsrc/grabber/audio/CMakeLists.txt
@@ -0,0 +1,35 @@
+# Define the current source locations
+SET( CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber )
+SET( CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio )
+
+
+if (WIN32)
+ FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Windows.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Windows.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp")
+elseif(${CMAKE_SYSTEM} MATCHES "Linux")
+ FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Linux.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Linux.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp")
+elseif (APPLE)
+ #TODO
+ #FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Apple.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Apple.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp")
+endif()
+
+add_library( audio-grabber ${AUDIO_GRABBER_SOURCES} )
+
+set(AUDIO_LIBS hyperion)
+
+
+if (WIN32)
+ set(AUDIO_LIBS ${AUDIO_LIBS} DSound)
+elseif(${CMAKE_SYSTEM} MATCHES "Linux")
+ find_package(ALSA REQUIRED)
+ if (ALSA_FOUND)
+ include_directories(${ALSA_INCLUDE_DIRS})
+ set(AUDIO_LIBS ${AUDIO_LIBS} ${ALSA_LIBRARIES})
+ endif(ALSA_FOUND)
+
+ set(THREADS_PREFER_PTHREAD_FLAG ON)
+ find_package(Threads REQUIRED)
+ set(AUDIO_LIBS ${AUDIO_LIBS} Threads::Threads) # PRIVATE
+endif()
+
+
+target_link_libraries(audio-grabber ${AUDIO_LIBS} ${QT_LIBRARIES})
diff --git a/libsrc/hyperion/CaptureCont.cpp b/libsrc/hyperion/CaptureCont.cpp
index 3cb566ed..4f3a7aae 100644
--- a/libsrc/hyperion/CaptureCont.cpp
+++ b/libsrc/hyperion/CaptureCont.cpp
@@ -20,6 +20,10 @@ CaptureCont::CaptureCont(Hyperion* hyperion)
, _v4lCaptPrio(0)
, _v4lCaptName()
, _v4lInactiveTimer(new QTimer(this))
+ , _audioCaptEnabled(false)
+ , _audioCaptPrio(0)
+ , _audioCaptName()
+ , _audioInactiveTimer(new QTimer(this))
{
// settings changes
connect(_hyperion, &Hyperion::settingsChanged, this, &CaptureCont::handleSettingsUpdate);
@@ -37,6 +41,11 @@ CaptureCont::CaptureCont(Hyperion* hyperion)
_v4lInactiveTimer->setSingleShot(true);
_v4lInactiveTimer->setInterval(1000);
+ // inactive timer audio
+ connect(_audioInactiveTimer, &QTimer::timeout, this, &CaptureCont::setAudioInactive);
+ _audioInactiveTimer->setSingleShot(true);
+ _audioInactiveTimer->setInterval(1000);
+
// init
handleSettingsUpdate(settings::INSTCAPTURE, _hyperion->getSetting(settings::INSTCAPTURE));
}
@@ -65,6 +74,17 @@ void CaptureCont::handleSystemImage(const QString& name, const Image&
_hyperion->setInputImage(_systemCaptPrio, image);
}
+void CaptureCont::handleAudioImage(const QString& name, const Image& image)
+{
+ if (_audioCaptName != name)
+ {
+ _hyperion->registerInput(_audioCaptPrio, hyperion::COMP_AUDIO, "System", name);
+ _audioCaptName = name;
+ }
+ _audioInactiveTimer->start();
+ _hyperion->setInputImage(_audioCaptPrio, image);
+}
+
void CaptureCont::setSystemCaptureEnable(bool enable)
{
if(_systemCaptEnabled != enable)
@@ -111,24 +131,56 @@ void CaptureCont::setV4LCaptureEnable(bool enable)
}
}
+void CaptureCont::setAudioCaptureEnable(bool enable)
+{
+ if (_audioCaptEnabled != enable)
+ {
+ if (enable)
+ {
+ _hyperion->registerInput(_audioCaptPrio, hyperion::COMP_AUDIO);
+ connect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, this, &CaptureCont::handleAudioImage);
+ connect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, _hyperion, &Hyperion::forwardAudioProtoMessage);
+ }
+ else
+ {
+ disconnect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, this, 0);
+ _hyperion->clear(_audioCaptPrio);
+ _audioInactiveTimer->stop();
+ _audioCaptName = "";
+ }
+ _audioCaptEnabled = enable;
+ _hyperion->setNewComponentState(hyperion::COMP_AUDIO, enable);
+ emit GlobalSignals::getInstance()->requestSource(hyperion::COMP_AUDIO, int(_hyperion->getInstanceIndex()), enable);
+ }
+}
+
void CaptureCont::handleSettingsUpdate(settings::type type, const QJsonDocument& config)
{
if(type == settings::INSTCAPTURE)
{
const QJsonObject& obj = config.object();
+
if(_v4lCaptPrio != obj["v4lPriority"].toInt(240))
{
setV4LCaptureEnable(false); // clear prio
_v4lCaptPrio = obj["v4lPriority"].toInt(240);
}
+
if(_systemCaptPrio != obj["systemPriority"].toInt(250))
{
setSystemCaptureEnable(false); // clear prio
_systemCaptPrio = obj["systemPriority"].toInt(250);
}
+ if (_audioCaptPrio != obj["audioPriority"].toInt(230))
+ {
+ setAudioCaptureEnable(false); // clear prio
+ _audioCaptPrio = obj["audioPriority"].toInt(230);
+ }
+
setV4LCaptureEnable(obj["v4lEnable"].toBool(false));
setSystemCaptureEnable(obj["systemEnable"].toBool(false));
+ setAudioCaptureEnable(obj["audioEnable"].toBool(true));
}
}
@@ -142,6 +194,10 @@ void CaptureCont::handleCompStateChangeRequest(hyperion::Components component, b
{
setV4LCaptureEnable(enable);
}
+ else if (component == hyperion::COMP_AUDIO)
+ {
+ setAudioCaptureEnable(enable);
+ }
}
void CaptureCont::setV4lInactive()
@@ -153,3 +209,8 @@ void CaptureCont::setSystemInactive()
{
_hyperion->setInputInactive(_systemCaptPrio);
}
+
+void CaptureCont::setAudioInactive()
+{
+ _hyperion->setInputInactive(_audioCaptPrio);
+}
diff --git a/libsrc/hyperion/ComponentRegister.cpp b/libsrc/hyperion/ComponentRegister.cpp
index 65d0ac83..fd2f261d 100644
--- a/libsrc/hyperion/ComponentRegister.cpp
+++ b/libsrc/hyperion/ComponentRegister.cpp
@@ -20,6 +20,7 @@ ComponentRegister::ComponentRegister(Hyperion* hyperion)
bool areScreenGrabberAvailable = !GrabberWrapper::availableGrabbers(GrabberTypeFilter::VIDEO).isEmpty();
bool areVideoGrabberAvailable = !GrabberWrapper::availableGrabbers(GrabberTypeFilter::VIDEO).isEmpty();
+ bool areAudioGrabberAvailable = !GrabberWrapper::availableGrabbers(GrabberTypeFilter::AUDIO).isEmpty();
bool flatBufServerAvailable { false };
bool protoBufServerAvailable{ false };
@@ -36,6 +37,11 @@ ComponentRegister::ComponentRegister(Hyperion* hyperion)
vect << COMP_GRABBER;
}
+ if (areAudioGrabberAvailable)
+ {
+ vect << COMP_AUDIO;
+ }
+
if (areVideoGrabberAvailable)
{
vect << COMP_V4L;
diff --git a/libsrc/hyperion/GrabberWrapper.cpp b/libsrc/hyperion/GrabberWrapper.cpp
index 1c846aa4..4d88a6f2 100644
--- a/libsrc/hyperion/GrabberWrapper.cpp
+++ b/libsrc/hyperion/GrabberWrapper.cpp
@@ -18,8 +18,10 @@ const int GrabberWrapper::DEFAULT_PIXELDECIMATION = 8;
/// Map of Hyperion instances with grabber name that requested screen capture
QMap GrabberWrapper::GRABBER_SYS_CLIENTS = QMap();
QMap GrabberWrapper::GRABBER_V4L_CLIENTS = QMap();
+QMap GrabberWrapper::GRABBER_AUDIO_CLIENTS = QMap();
bool GrabberWrapper::GLOBAL_GRABBER_SYS_ENABLE = false;
bool GrabberWrapper::GLOBAL_GRABBER_V4L_ENABLE = false;
+bool GrabberWrapper::GLOBAL_GRABBER_AUDIO_ENABLE = false;
GrabberWrapper::GrabberWrapper(const QString& grabberName, Grabber * ggrabber, int updateRate_Hz)
: _grabberName(grabberName)
@@ -38,9 +40,12 @@ GrabberWrapper::GrabberWrapper(const QString& grabberName, Grabber * ggrabber, i
connect(_timer, &QTimer::timeout, this, &GrabberWrapper::action);
// connect the image forwarding
- (_grabberName.startsWith("V4L"))
- ? connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setV4lImage)
- : connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setSystemImage);
+ if (_grabberName.startsWith("V4L"))
+ connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setV4lImage);
+ else if (_grabberName.startsWith("Audio"))
+ connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setAudioImage);
+ else
+ connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setSystemImage);
// listen for source requests
connect(GlobalSignals::getInstance(), &GlobalSignals::requestSource, this, &GrabberWrapper::handleSourceRequest);
@@ -99,6 +104,12 @@ QStringList GrabberWrapper::getActive(int inst, GrabberTypeFilter type) const
result << GRABBER_V4L_CLIENTS.value(inst);
}
+ if (type == GrabberTypeFilter::AUDIO || type == GrabberTypeFilter::ALL)
+ {
+ if (GRABBER_AUDIO_CLIENTS.contains(inst))
+ result << GRABBER_AUDIO_CLIENTS.value(inst);
+ }
+
return result;
}
@@ -148,6 +159,13 @@ QStringList GrabberWrapper::availableGrabbers(GrabberTypeFilter type)
#endif
}
+ if (type == GrabberTypeFilter::AUDIO || type == GrabberTypeFilter::ALL)
+ {
+ #ifdef ENABLE_AUDIO
+ grabbers << "audio";
+ #endif
+ }
+
return grabbers;
}
@@ -187,7 +205,9 @@ void GrabberWrapper::updateTimer(int interval)
void GrabberWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument& config)
{
- if(type == settings::SYSTEMCAPTURE && !_grabberName.startsWith("V4L"))
+ if (type == settings::SYSTEMCAPTURE &&
+ !_grabberName.startsWith("V4L") &&
+ !_grabberName.startsWith("Audio"))
{
// extract settings
const QJsonObject& obj = config.object();
@@ -235,26 +255,42 @@ void GrabberWrapper::handleSettingsUpdate(settings::type type, const QJsonDocume
void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyperionInd, bool listen)
{
- if(component == hyperion::Components::COMP_GRABBER && !_grabberName.startsWith("V4L"))
+ if (component == hyperion::Components::COMP_GRABBER &&
+ !_grabberName.startsWith("V4L") &&
+ !_grabberName.startsWith("Audio"))
{
- if(listen)
+ if (listen)
GRABBER_SYS_CLIENTS.insert(hyperionInd, _grabberName);
else
GRABBER_SYS_CLIENTS.remove(hyperionInd);
- if(GRABBER_SYS_CLIENTS.empty() || !getSysGrabberState())
+ if (GRABBER_SYS_CLIENTS.empty() || !getSysGrabberState())
stop();
else
start();
}
- else if(component == hyperion::Components::COMP_V4L && _grabberName.startsWith("V4L"))
+ else if (component == hyperion::Components::COMP_V4L &&
+ _grabberName.startsWith("V4L"))
{
- if(listen)
+ if (listen)
GRABBER_V4L_CLIENTS.insert(hyperionInd, _grabberName);
else
GRABBER_V4L_CLIENTS.remove(hyperionInd);
- if(GRABBER_V4L_CLIENTS.empty() || !getV4lGrabberState())
+ if (GRABBER_V4L_CLIENTS.empty() || !getV4lGrabberState())
+ stop();
+ else
+ start();
+ }
+ else if (component == hyperion::Components::COMP_AUDIO &&
+ _grabberName.startsWith("Audio"))
+ {
+ if (listen)
+ GRABBER_AUDIO_CLIENTS.insert(hyperionInd, _grabberName);
+ else
+ GRABBER_AUDIO_CLIENTS.remove(hyperionInd);
+
+ if (GRABBER_AUDIO_CLIENTS.empty())
stop();
else
start();
@@ -264,6 +300,11 @@ void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyp
void GrabberWrapper::tryStart()
{
// verify start condition
- if(!_grabberName.startsWith("V4L") && !GRABBER_SYS_CLIENTS.empty() && getSysGrabberState())
+ if (!_grabberName.startsWith("V4L") &&
+ !_grabberName.startsWith("Audio") &&
+ !GRABBER_SYS_CLIENTS.empty() &&
+ getSysGrabberState())
+ {
start();
+ }
}
diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp
index e8e444a9..b9d78edc 100644
--- a/libsrc/hyperion/SettingsManager.cpp
+++ b/libsrc/hyperion/SettingsManager.cpp
@@ -501,6 +501,20 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
Debug(_log, "GrabberV4L2 records migrated");
}
+ if (config.contains("grabberAudio"))
+ {
+ QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject();
+
+ //Add new element enable
+ if (!newGrabberAudioConfig.contains("enable"))
+ {
+ newGrabberAudioConfig["enable"] = false;
+ migrated = true;
+ }
+ config["grabberAudio"] = newGrabberAudioConfig;
+ Debug(_log, "GrabberAudio records migrated");
+ }
+
if (config.contains("framegrabber"))
{
QJsonObject newFramegrabberConfig = config["framegrabber"].toObject();
diff --git a/libsrc/hyperion/hyperion.schema.json b/libsrc/hyperion/hyperion.schema.json
index 36541d6b..12e1ba73 100644
--- a/libsrc/hyperion/hyperion.schema.json
+++ b/libsrc/hyperion/hyperion.schema.json
@@ -27,6 +27,10 @@
{
"$ref": "schema-grabberV4L2.json"
},
+ "grabberAudio" :
+ {
+ "$ref": "schema-grabberAudio.json"
+ },
"framegrabber" :
{
"$ref": "schema-framegrabber.json"
diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc
index fdc66d5f..f1c327cf 100644
--- a/libsrc/hyperion/resource.qrc
+++ b/libsrc/hyperion/resource.qrc
@@ -7,6 +7,7 @@
schema/schema-color.json
schema/schema-smoothing.json
schema/schema-grabberV4L2.json
+ schema/schema-grabberAudio.json
schema/schema-framegrabber.json
schema/schema-blackborderdetector.json
schema/schema-foregroundEffect.json
diff --git a/libsrc/hyperion/schema/schema-grabberAudio.json b/libsrc/hyperion/schema/schema-grabberAudio.json
new file mode 100644
index 00000000..b4225872
--- /dev/null
+++ b/libsrc/hyperion/schema/schema-grabberAudio.json
@@ -0,0 +1,163 @@
+{
+ "type": "object",
+ "required": true,
+ "title": "edt_conf_audio_heading_title",
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "title": "edt_conf_general_enable_title",
+ "required": true,
+ "default": false,
+ "propertyOrder": 1
+ },
+ "available_devices": {
+ "type": "string",
+ "title": "edt_conf_grabber_discovered_title",
+ "default": "edt_conf_grabber_discovery_inprogress",
+ "options": {
+ "infoText": "edt_conf_grabber_discovered_title_info"
+ },
+ "propertyOrder": 2,
+ "required": false
+ },
+ "device": {
+ "type": "string",
+ "title": "edt_conf_enum_custom",
+ "default": "auto",
+ "options": {
+ "hidden": true
+ },
+ "required": true,
+ "propertyOrder": 3,
+ "comment": "The 'available_audio_devices' settings are dynamically inserted into the WebUI under PropertyOrder '1'."
+ },
+ "audioEffect": {
+ "type": "string",
+ "title": "edt_conf_audio_effects_title",
+ "required": true,
+ "enum": [ "vuMeter" ],
+ "default": "vuMeter",
+ "options": {
+ "enum_titles": [ "edt_conf_audio_effect_enum_vumeter" ]
+ },
+ "propertyOrder": 4
+ },
+ "vuMeter": {
+ "type": "object",
+ "title": "",
+ "required": true,
+ "propertyOrder": 5,
+ "options": {
+ "dependencies": {
+ "audioEffect": "vuMeter"
+ }
+ },
+ "properties": {
+ "multiplier": {
+ "type": "number",
+ "title": "edt_conf_audio_effect_multiplier_title",
+ "default": 1,
+ "minimum": 0,
+ "step": 0.01,
+ "required": true,
+ "propertyOrder": 1,
+ "comment": "The multiplier is used to scale the audio input signal. Increase or decrease to achieve the desired effect. Set to 0 for auto"
+ },
+ "tolerance": {
+ "type": "number",
+ "title": "edt_conf_audio_effect_tolerance_title",
+ "default": 5,
+ "minimum": 0,
+ "step": 1,
+ "append": "edt_append_percent",
+ "required": true,
+ "propertyOrder": 2,
+ "comment": "The tolerance is a percentage value from 0 - 100 used during auto multiplier calculation."
+ },
+ "hotColor": {
+ "type": "array",
+ "title": "edt_conf_audio_effect_hotcolor_title",
+ "default": [ 255, 0, 0 ],
+ "format": "colorpicker",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "required": true,
+ "propertyOrder": 3,
+ "comment": "Hot Color is the color the led's will reach when audio level exceeds the warn percentage"
+ },
+ "warnColor": {
+ "type": "array",
+ "title": "edt_conf_audio_effect_warncolor_title",
+ "default": [ 255, 255, 0 ],
+ "format": "colorpicker",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "required": true,
+ "propertyOrder": 4,
+ "comment": "Warn Color is the color the led's will reach when audio level exceeds the safe percentage"
+ },
+ "warnValue": {
+ "type": "number",
+ "title": "edt_conf_audio_effect_warnvalue_title",
+ "default": 80,
+ "minimum": 0,
+ "step": 1,
+ "append": "edt_append_percent",
+ "required": true,
+ "propertyOrder": 5,
+ "comment": "Warn percentage is the percentage used to determine the maximum percentage of the audio warning level"
+ },
+ "safeColor": {
+ "type": "array",
+ "title": "edt_conf_audio_effect_safecolor_title",
+ "default": [ 0, 255, 0 ],
+ "format": "colorpicker",
+ "items": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "minItems": 3,
+ "maxItems": 3,
+ "required": true,
+ "propertyOrder": 6,
+ "comment": "Safe Color is the color the led's will reach when audio level is below the warning percentage"
+ },
+ "safeValue": {
+ "type": "number",
+ "title": "edt_conf_audio_effect_safevalue_title",
+ "default": 45,
+ "minimum": 0,
+ "step": 1,
+ "append": "edt_append_percent",
+ "required": true,
+ "propertyOrder": 7,
+ "comment": "Safe percentage is the percentage used to determine the maximum percentage of the audio safe level"
+ },
+ "flip": {
+ "type": "string",
+ "title": "edt_conf_v4l2_flip_title",
+ "enum": [ "NO_CHANGE", "HORIZONTAL", "VERTICAL", "BOTH" ],
+ "default": "NO_CHANGE",
+ "options": {
+ "enum_titles": [ "edt_conf_enum_NO_CHANGE", "edt_conf_enum_HORIZONTAL", "edt_conf_enum_VERTICAL", "edt_conf_enum_BOTH" ]
+ },
+ "required": true,
+ "access": "advanced",
+ "propertyOrder": 8
+ }
+ }
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/libsrc/hyperion/schema/schema-instCapture.json b/libsrc/hyperion/schema/schema-instCapture.json
index 28fe9f4c..a4076a6a 100644
--- a/libsrc/hyperion/schema/schema-instCapture.json
+++ b/libsrc/hyperion/schema/schema-instCapture.json
@@ -48,6 +48,29 @@
"maximum": 253,
"default": 240,
"propertyOrder": 6
+ },
+ "audioEnable": {
+ "type": "boolean",
+ "required": true,
+ "title": "edt_conf_instC_audioEnable_title",
+ "default": false,
+ "propertyOrder": 7
+ },
+ "audioGrabberDevice": {
+ "type": "string",
+ "required": true,
+ "title": "edt_conf_instC_video_grabber_device_title",
+ "default": "NONE",
+ "propertyOrder": 7
+ },
+ "audioPriority": {
+ "type": "integer",
+ "required": true,
+ "title": "edt_conf_general_priority_title",
+ "minimum": 100,
+ "maximum": 253,
+ "default": 230,
+ "propertyOrder": 9
}
},
"additionalProperties" : false
diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp
index edb49a5b..5cec2ec3 100644
--- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp
+++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp
@@ -162,7 +162,8 @@ void QJsonSchemaChecker::validate(const QJsonValue& value, const QJsonObject& sc
; // references have already been collected
else if (attribute == "title" || attribute == "description" || attribute == "default" || attribute == "format"
|| attribute == "defaultProperties" || attribute == "propertyOrder" || attribute == "append" || attribute == "step"
- || attribute == "access" || attribute == "options" || attribute == "script" || attribute == "allowEmptyArray" || attribute == "comment")
+ || attribute == "access" || attribute == "options" || attribute == "script" || attribute == "allowEmptyArray" || attribute == "comment"
+ || attribute == "watch" || attribute == "template")
; // nothing to do.
else
{
diff --git a/src/hyperion-remote/hyperion-remote.cpp b/src/hyperion-remote/hyperion-remote.cpp
index 37d2ec03..f47fbb14 100644
--- a/src/hyperion-remote/hyperion-remote.cpp
+++ b/src/hyperion-remote/hyperion-remote.cpp
@@ -113,8 +113,8 @@ int main(int argc, char * argv[])
#endif
BooleanOption & argClear = parser.add('x', "clear" , "Clear data for the priority channel provided by the -p option");
BooleanOption & argClearAll = parser.add(0x0, "clearall" , "Clear data for all active priority channels");
- Option & argEnableComponent = parser.add