Merge branch 'development'
@ -61,23 +61,6 @@ DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentCaseLabels: false
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
@ -104,7 +87,7 @@ PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 1000
|
||||
PointerAlignment: Right
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
|
@ -48,4 +48,6 @@ Checks: "*,\
|
||||
-google-readability-braces-around-statements,\
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,\
|
||||
-cppcoreguidelines-pro-type-union-access,\
|
||||
-readability-static-accessed-through-instance"
|
||||
-readability-static-accessed-through-instance,\
|
||||
-cppcoreguidelines-special-member-functions,\
|
||||
-readability-isolate-declaration"
|
22
.github/CONTRIBUTING.md
vendored
@ -2,14 +2,26 @@
|
||||
|
||||
Contributions are welcome! Here's how you can help:
|
||||
|
||||
- [Translating](#translations)
|
||||
- [Contributing code](#code)
|
||||
- [Reporting issues](#issues)
|
||||
- [Donating](#donations)
|
||||
- [Translations](#translations)
|
||||
- [Code](#code)
|
||||
- [Issues](#issues)
|
||||
- [Donations](#donations)
|
||||
|
||||
## Translations
|
||||
|
||||
[Internationalization](https://github.com/Soundux/Soundux/issues/52) is not yet implemented. When we have a way to do this, we add the instructions here.
|
||||
1. [Fork the frontend](https://github.com/Soundux/soundux-ui/fork) and [clone](https://help.github.com/articles/cloning-a-repository/) your fork.
|
||||
2. Start translating!
|
||||
- Add a translation file in `/src/locales/`
|
||||
- If you are adding a translation which language doesn't exist yet, name your translation `[COUNTRY_CODE].js`
|
||||
- If there already is a translation for your language and you want to add a territory specific one, name your translation `[COUNTRY_CODE]-[TERRITORY].js` so that it plays nicely with the [Implicit fallback](https://kazupon.github.io/vue-i18n/guide/fallback.html#implicit-fallback-using-locales)
|
||||
- Replace `[COUNTRY_CODE]` with your corresponding code. [See the list here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (Use column `639-1`)
|
||||
- Replace `[TERRITORY]` with your corresponding territory code. [See the list here](https://en.wikipedia.org/wiki/ISO_3166-1#Officially_assigned_code_elements) (Use column `Alpha-2 code`)
|
||||
- Add the corresponding translations for your language
|
||||
|
||||
3. Commit your changes to a new branch (not `master`, one change per branch) and push it:
|
||||
- Use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716)
|
||||
|
||||
4. Once you are happy with your translation, submit a pull request.
|
||||
|
||||
## Code
|
||||
|
||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,6 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Affected part of the program**
|
||||
- [ ] Frontend <i>(e.g Something in the UI is not behaving correctly)</i>
|
||||
- [ ] Backend <i>(e.g Crashes, Problems with Audio-Playback)</i>
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
4
.github/dependabot.yml
vendored
@ -9,3 +9,7 @@ updates:
|
||||
directory: '/lib'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
- package-ecosystem: 'gitsubmodule'
|
||||
directory: '/src/ui/impl/webview/lib'
|
||||
schedule:
|
||||
interval: 'daily'
|
12
.github/workflows/build_flatpak.yml
vendored
@ -1,21 +1,19 @@
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- "**/README.md"
|
||||
- "**/docs/**"
|
||||
- "**/release_windows.yml"
|
||||
- "**/compile_windows.yml"
|
||||
- '**/compile_linux.yml'
|
||||
- '**/codeql-analysis-python.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [ master ]
|
||||
|
||||
name: Build Flatpak
|
||||
jobs:
|
||||
flatpak-builder:
|
||||
runs-on: ubuntu-20.04
|
||||
container:
|
||||
image: bilelmoussaoui/flatpak-github-actions:kde-5.15
|
||||
image: bilelmoussaoui/flatpak-github-actions:gnome-3.38
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
@ -23,4 +21,4 @@ jobs:
|
||||
- uses: bilelmoussaoui/flatpak-github-actions@v2
|
||||
with:
|
||||
bundle: "soundux.flatpak"
|
||||
manifest-path: "io.github.Soundux.yml"
|
||||
manifest-path: "assets/flatpak/io.github.Soundux.yml"
|
||||
|
36
.github/workflows/codeql-analysis-python.yml
vendored
@ -1,36 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
- '**/docs/**'
|
||||
- '**/release_windows.yml'
|
||||
- '**/compile_linux.yml'
|
||||
- '**/build_flatpak.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
29
.github/workflows/compile_linux.yml
vendored
@ -3,10 +3,6 @@ on:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
- '**/docs/**'
|
||||
- '**/release_windows.yml'
|
||||
- '**/build_flatpak.yml'
|
||||
- '**/codeql-analysis-python.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
@ -23,25 +19,8 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
version: '5.15.2'
|
||||
dir: '${{ github.workspace }}'
|
||||
|
||||
- name: Install other build dependencies
|
||||
run: 'sudo apt-get install libx11-dev libxi-dev'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
submodules: recursive
|
||||
- name: Install build dependencies
|
||||
run: 'sudo apt-get install git build-essential cmake libx11-dev libxi-dev libwebkit2gtk-4.0-dev npm'
|
||||
- name: Compile
|
||||
run: 'mkdir build && cd build && cmake .. && make'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
run: 'mkdir build && cd build && cmake .. && cmake --build . --config Release'
|
||||
|
33
.github/workflows/compile_windows.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
name: Build on Windows
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'cpp' ]
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.5
|
||||
- name: Setup NuGet.exe for use with actions
|
||||
uses: NuGet/setup-nuget@v1.0.5
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Compile
|
||||
run: 'mkdir build && cd build && cmake .. && cmake --build . --config Release'
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
name: Artifact
|
||||
path: build
|
78
.github/workflows/release_windows.yml
vendored
@ -1,78 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
- '**/docs/**'
|
||||
- '**/compile_linux.yml'
|
||||
- '**/build_flatpak.yml'
|
||||
- '**/codeql-analysis-python.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
name: Build on Windows
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
working-directory: .
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
dir: '${{ github.workspace }}/qt/'
|
||||
|
||||
- name: Compile
|
||||
run: 'mkdir build && cd build && cmake .. && cmake --build . --target ALL_BUILD --config Release'
|
||||
|
||||
- name: Run Windeploy
|
||||
run: 'cd build/Release && windeployqt --release --qmldir ../../src/qml soundux.exe'
|
||||
|
||||
- name: Install InnoSetup
|
||||
uses: crazy-max/ghaction-chocolatey@v1
|
||||
with:
|
||||
args: install innosetup -y
|
||||
|
||||
- name: Install Sed
|
||||
uses: crazy-max/ghaction-chocolatey@v1
|
||||
with:
|
||||
args: install sed -y
|
||||
|
||||
- name: Install 7zip
|
||||
uses: crazy-max/ghaction-chocolatey@v1
|
||||
with:
|
||||
args: install 7zip -y
|
||||
|
||||
- name: Install Wget
|
||||
uses: crazy-max/ghaction-chocolatey@v1
|
||||
with:
|
||||
args: install wget -y
|
||||
|
||||
- name: Run Sed
|
||||
run: '&"${Env:ProgramData}\chocolatey\bin\sed.exe" -i ''s%\$PATH%D:\\a\\Soundux\\Soundux%'' src/innosetup/Soundux.iss'
|
||||
|
||||
- name: Download VB-Cable
|
||||
run: '&"${Env:ProgramData}\chocolatey\bin\wget.exe" https://download.vb-audio.com/Download_CABLE/VBCABLE_Driver_Pack43.zip'
|
||||
|
||||
- name: Unpack VB-Cable
|
||||
run: '&"${Env:ProgramData}\chocolatey\bin\7z.exe" x .\VBCABLE_Driver_Pack43.zip -o*'
|
||||
|
||||
- name: Run InnoSetup
|
||||
run: '&"${Env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" src/innosetup/Soundux.iss'
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
name: Release
|
||||
path: build/Release
|
||||
|
||||
- name: Upload Installer Artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
name: Installer
|
||||
path: src/innosetup/Output/setup.exe
|
2
.gitignore
vendored
@ -3,4 +3,4 @@ cmake-build-debug
|
||||
.idea
|
||||
.vscode
|
||||
.history
|
||||
src/qml/resources/*.js
|
||||
.cache
|
19
.gitmodules
vendored
@ -4,6 +4,19 @@
|
||||
[submodule "lib/miniaudio"]
|
||||
path = lib/miniaudio
|
||||
url = https://github.com/mackron/miniaudio
|
||||
[submodule "lib/materialdesignicons"]
|
||||
path = lib/materialdesignicons
|
||||
url = https://github.com/Templarian/MaterialDesign-Webfont
|
||||
[submodule "lib/fancypp"]
|
||||
path = lib/fancypp
|
||||
url = https://github.com/Curve/fancypp
|
||||
[submodule "src/ui/impl/webview/lib/webview"]
|
||||
path = src/ui/impl/webview/lib/webview
|
||||
url = https://github.com/Soundux/webview
|
||||
[submodule "lib/nativefiledialog-extended"]
|
||||
path = lib/nativefiledialog-extended
|
||||
url = https://github.com/btzy/nativefiledialog-extended/
|
||||
[submodule "lib/InstanceGuard"]
|
||||
path = lib/InstanceGuard
|
||||
url = https://github.com/Grandbrain/InstanceGuard
|
||||
[submodule "src/ui/impl/webview/lib/soundux-ui"]
|
||||
path = src/ui/impl/webview/lib/soundux-ui
|
||||
url = https://github.com/Soundux/soundux-ui/
|
||||
branch = build
|
||||
|
@ -1,81 +1,51 @@
|
||||
cmake_minimum_required(VERSION 3.1)
|
||||
project(soundux VERSION 0.1.6 DESCRIPTION "A cross-platform soundboard in QtQuick")
|
||||
project(soundux VERSION 1.0 DESCRIPTION "")
|
||||
|
||||
# Options
|
||||
option(SOUNDUX_DEBUGINFO "Compiles with debug symbols to provide useful information for debugging" Off)
|
||||
if (SOUNDUX_DEBUGINFO)
|
||||
message("Compiling with debug info")
|
||||
set(CMAKE_BUILD_TYPE RelWithDebInfo)
|
||||
endif()
|
||||
# -------
|
||||
|
||||
# Generate Material Design Js
|
||||
if (NOT EXISTS "${CMAKE_SOURCE_DIR}/src/qml/resources/MaterialDesign.js")
|
||||
if (MSVC)
|
||||
execute_process(COMMAND cmd /c python "${CMAKE_SOURCE_DIR}/lib/generateMaterialDesignJs.py"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")
|
||||
else()
|
||||
execute_process(COMMAND python3 "${CMAKE_SOURCE_DIR}/lib/generateMaterialDesignJs.py"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")
|
||||
endif()
|
||||
|
||||
message("Generated MaterialDesign.js")
|
||||
endif()
|
||||
# ---------------------------
|
||||
|
||||
# QT-Related
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
find_package(Qt5 COMPONENTS Widgets Qml Quick QuickControls2 REQUIRED)
|
||||
qt5_add_resources(QT_RESOURCES src/qml/qml.qrc)
|
||||
# ----------
|
||||
|
||||
# Source Files to compile
|
||||
file(GLOB src
|
||||
"src/*.cpp"
|
||||
"src/*.qml"
|
||||
"src/*/*.cpp"
|
||||
"src/*/*/*.cpp"
|
||||
"src/*/*/*/*.cpp"
|
||||
"src/*/*/*/*/*.cpp"
|
||||
)
|
||||
# -----------------------
|
||||
|
||||
# Linux Dependencies
|
||||
if (UNIX AND NOT APPLE)
|
||||
find_package(X11 REQUIRED)
|
||||
if (WIN32)
|
||||
add_executable(soundux WIN32 ${src})
|
||||
else()
|
||||
add_executable(soundux ${src})
|
||||
endif()
|
||||
# ------------------
|
||||
# Unix Dependencies
|
||||
if (UNIX)
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
endif()
|
||||
# ------------------
|
||||
|
||||
add_executable(soundux ${src} ${QT_RESOURCES})
|
||||
|
||||
target_include_directories(soundux PRIVATE ${Qt5Widgets_INCLUDE_DIRS} ${QtQml_INCLUDE_DIRS})
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_include_directories(soundux PRIVATE ${X11_INCLUDE_DIR} ${X11_Xinput_INCLUDE_PATH})
|
||||
endif()
|
||||
target_include_directories(soundux PRIVATE "lib/json/single_include/nlohmann")
|
||||
target_include_directories(soundux PRIVATE "lib/object_threadsafe")
|
||||
target_include_directories(soundux PRIVATE "lib/miniaudio")
|
||||
target_include_directories(soundux PRIVATE "lib/fancypp/include")
|
||||
target_include_directories(soundux PRIVATE "lib/json/single_include")
|
||||
target_include_directories(soundux PRIVATE "lib/InstanceGuard/Source")
|
||||
|
||||
target_compile_definitions(soundux PRIVATE ${Qt5Widgets_DEFINITIONS} ${QtQml_DEFINITIONS} ${Qt5Quick_DEFINITIONS})
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(soundux PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_libraries(soundux ${X11_LIBRARIES} ${X11_Xinput_LIB} ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if (UNIX)
|
||||
target_link_libraries(soundux pthread)
|
||||
find_package(X11 REQUIRED)
|
||||
include_directories(${X11_INCLUDE_DIR})
|
||||
target_link_libraries(soundux PRIVATE ${X11_LIBRARIES} ${X11_Xinput_LIB})
|
||||
endif()
|
||||
if (WIN32)
|
||||
target_compile_definitions(soundux PRIVATE _CRT_SECURE_NO_WARNINGS=1 _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS=1 _UNICODE=1)
|
||||
endif()
|
||||
|
||||
target_link_libraries(soundux
|
||||
Qt5::Widgets
|
||||
Qt5::Qml
|
||||
Qt5::Quick
|
||||
Qt5::QuickControls2
|
||||
)
|
||||
add_subdirectory(src/ui/impl/webview/lib/webview EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(lib/nativefiledialog-extended EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(lib/InstanceGuard/Source EXCLUDE_FROM_ALL)
|
||||
target_link_libraries(soundux PUBLIC webview nfd InstanceGuard)
|
||||
|
||||
# [[ Build Frontend ]]
|
||||
if (MSVC)
|
||||
file(COPY "${CMAKE_SOURCE_DIR}/src/ui/impl/webview/lib/soundux-ui/"
|
||||
DESTINATION "${CMAKE_SOURCE_DIR}/build/Release/dist")
|
||||
else()
|
||||
file(COPY "${CMAKE_SOURCE_DIR}/src/ui/impl/webview/lib/soundux-ui/"
|
||||
DESTINATION "${CMAKE_SOURCE_DIR}/build/dist")
|
||||
endif()
|
||||
|
||||
target_compile_features(soundux PRIVATE cxx_std_17)
|
||||
set_target_properties(soundux PROPERTIES CMAKE_CXX_STANDARD 17)
|
||||
@ -86,3 +56,4 @@ set_target_properties(soundux PROPERTIES VERSION ${PROJECT_VERSION})
|
||||
set_target_properties(soundux PROPERTIES PROJECT_NAME ${PROJECT_NAME})
|
||||
|
||||
install(TARGETS soundux DESTINATION bin)
|
||||
install(DIRECTORY "${CMAKE_SOURCE_DIR}/build/dist" DESTINATION bin)
|
34
README.md
@ -1,6 +1,13 @@
|
||||

|
||||
<div align="center">
|
||||
<p>
|
||||
<img src="assets/logo.gif" height="200"/>
|
||||
<br>
|
||||
<h6>A cross-platform soundboard 🔊</h6>
|
||||
<br>
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/Soundux/soundux?style=flat-square">
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/Soundux/soundux?style=flat-square">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr-raw/Soundux/soundux?label=pulls&style=flat-square">
|
||||
<br>
|
||||
<a href="https://github.com/Soundux/Soundux/releases">
|
||||
<img src="https://img.shields.io/github/release/Soundux/Soundux.svg?style=flat-square" alt="Latest Stable Release" />
|
||||
</a>
|
||||
@ -24,7 +31,10 @@
|
||||
</div>
|
||||
|
||||
# Preview
|
||||

|
||||
|  |  |
|
||||
| ---------------------------------------------------- | ------------------------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
# Introduction
|
||||
Soundux is a cross-platform soundboard that features a simple user interface.
|
||||
@ -37,8 +47,10 @@ These are required to run the program
|
||||
Please refer to your distro instructions on how to install
|
||||
- [pulseaudio](https://gitlab.freedesktop.org/pulseaudio/pulseaudio)
|
||||
- Xorg
|
||||
- Webkit2gtk
|
||||
## Windows
|
||||
- [VB-CABLE](https://vb-audio.com/Cable/) (Our installer automatically installs VB-CABLE)
|
||||
- [Webview2 Runtime](https://developer.microsoft.com/de-de/microsoft-edge/webview2/) (Is also shipped with the installer)
|
||||
|
||||
# Installation
|
||||
|
||||
@ -67,25 +79,17 @@ Download our installer or portable from [the latest release](https://github.com/
|
||||
## Build Dependencies
|
||||
|
||||
### Linux
|
||||
This list may not be accurate. Contact me if you find missing dependencies that I can update this list
|
||||
- [qt5-base](https://github.com/qt/qtbase) >=5.15
|
||||
- [qt5-tools](https://github.com/qt/qt5) >=5.15
|
||||
- [qt5-quickcontrols2](https://github.com/qt/qtquickcontrols2) >=5.15
|
||||
This list may not be accurate. Contact me if you find missing dependencies so that I can update this list
|
||||
- Webkit2gtk
|
||||
- X11 client-side development headers
|
||||
|
||||
<b>Qt >= 5.15 is strictly required!</b>
|
||||
|
||||
#### Ubuntu and derivatives
|
||||
#### Debian/Ubuntu and derivatives
|
||||
```sh
|
||||
sudo apt install git build-essential cmake libx11-dev libqt5x11extras5-dev libxi-dev
|
||||
sudo apt install git build-essential cmake libx11-dev libxi-dev libwebkit2gtk-4.0-dev
|
||||
```
|
||||
Ubuntu does not have Qt 5.15 in its repositories so you need to use their [Online Installer](https://www.qt.io/download-thank-you?hsLang=en) or [compile it from source](https://doc.qt.io/qt-5/build-sources.html#linux-x11)
|
||||
|
||||
### Windows
|
||||
*(We highly recommend you to just download the latest release for windows since it has all its dependencies packed with it)*
|
||||
|
||||
To compile on windows you'll have to install qt (*make sure the the important qt-paths are in your system-path!*)
|
||||
- [Qt](https://www.qt.io/download-thank-you?os=windows)
|
||||
- Nuget
|
||||
- MSVC
|
||||
- CMake
|
||||
|
||||
|
BIN
assets/flatpak/icons/io.github.Soundux-256.png
Normal file
After Width: | Height: | Size: 21 KiB |
@ -8,7 +8,7 @@
|
||||
<summary>A cross-platform soundboard in QtQuick</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-only</project_license>
|
||||
<url type="homepage">https://soundux.github.io/</url>
|
||||
<url type="homepage">https://soundux.rocks/</url>
|
||||
<url type="donation">https://ko-fi.com/soundux/</url>
|
||||
<url type="bugtracker">https://github.com/Soundux/Soundux/issues</url>
|
||||
<url type="help">https://github.com/Soundux/Soundux/issues</url>
|
||||
@ -57,6 +57,7 @@
|
||||
<li>Changelogs on Flathub</li>
|
||||
<li>General bugfixes and stability improvements</li>
|
||||
<li>Only unload PulseAudio modules created by Soundux</li>
|
||||
<li>Update stop button state</li>
|
||||
<li>Prevent opening the program multiple times</li>
|
||||
</ul>
|
||||
</description>
|
@ -1,7 +1,7 @@
|
||||
app-id: io.github.Soundux
|
||||
runtime: org.kde.Platform
|
||||
runtime-version: '5.15'
|
||||
sdk: org.kde.Sdk
|
||||
runtime: org.gnome.Platform
|
||||
runtime-version: '3.38'
|
||||
sdk: org.gnome.Sdk
|
||||
command: soundux
|
||||
finish-args:
|
||||
- --device=all
|
||||
@ -16,3 +16,4 @@ modules:
|
||||
sources:
|
||||
- type: git
|
||||
url: https://github.com/Soundux/Soundux.git
|
||||
branch: master
|
BIN
assets/icon.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
assets/logo.gif
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/screenshots/1.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
assets/screenshots/2.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
assets/screenshots/3.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
assets/screenshots/4.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
assets/screenshots/5.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
assets/screenshots/6.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
assets/screenshots/7.png
Normal file
After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 39 KiB |
1
lib/InstanceGuard
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 5e3efce2036d744c04f1b190536d7c57e567040d
|
1
lib/fancypp
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 19d0dc6c7dc63a6260a22b27ef5ac6eb2ffe0982
|
@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import math
|
||||
import re
|
||||
|
||||
print("Generating MaterialDesign.js")
|
||||
|
||||
# open css file from submodule
|
||||
css = open("materialdesignicons/css/materialdesignicons.css")
|
||||
print("Got css file!")
|
||||
|
||||
# lines to be written to js file
|
||||
lines = ["var icons = {"]
|
||||
|
||||
|
||||
# function to get the surrogate pair for js
|
||||
def get_surrogate_pair(astral_code_point: int) -> tuple:
|
||||
high_surrogate = math.floor((astral_code_point - 0x10000) / 0x400) + 0xD800
|
||||
low_surrogate = (astral_code_point - 0x10000) % 0x400 + 0xDC00
|
||||
return str(hex(high_surrogate)).replace("0x", "").upper(), str(hex(low_surrogate)).replace("0x", "").upper()
|
||||
|
||||
|
||||
for line in css:
|
||||
if "::before" in line:
|
||||
next_line = next(css)
|
||||
if "content:" in next_line:
|
||||
# found an icon
|
||||
icon_name = re.findall(r"mdi-[a-z-]+", line)[0].replace("-", "_")
|
||||
content_line = re.findall(r"\\[A-Z0-9]+", next_line)[0]
|
||||
hex_code = int(content_line.replace("\\F", "0xF"), 16)
|
||||
surrogate_pair = get_surrogate_pair(hex_code)
|
||||
js_pair = "\\u" + surrogate_pair[0] + "\\u" + surrogate_pair[1]
|
||||
# add icon line to js lines
|
||||
lines.append(" " + icon_name + ": \"" + js_pair + "\",")
|
||||
|
||||
lines.append("}")
|
||||
|
||||
# save to src js file
|
||||
new_file = open("../src/qml/resources/MaterialDesign.js", "w")
|
||||
for line in lines:
|
||||
new_file.write(line + "\n")
|
2
lib/json
@ -1 +1 @@
|
||||
Subproject commit db78ac1d7716f56fc9f1b030b715f872f93964e4
|
||||
Subproject commit b83fe5dbf2c1a64a766ca44dae0afd8095205adc
|
@ -1 +0,0 @@
|
||||
Subproject commit ca547d7878316031d24a1dbe5a9078693bb17517
|
1
lib/nativefiledialog-extended
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit fbd8480bd63b8b9d808e3206acccd1cb113bac8b
|
@ -1,2 +0,0 @@
|
||||
#include "bindings.h"
|
||||
// this file is needed for qt (MOC)
|
@ -1,117 +0,0 @@
|
||||
#pragma once
|
||||
#include <qlist.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
#include "../config/config.h"
|
||||
#include "../hotkeys/global.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include "../playback/linux.h"
|
||||
|
||||
struct QPulseAudioRecordingStream
|
||||
{
|
||||
Q_GADGET
|
||||
public:
|
||||
void setInstance(Soundux::Playback::internal::PulseAudioRecordingStream instance)
|
||||
{
|
||||
this->instance = std::move(instance);
|
||||
}
|
||||
Q_INVOKABLE QString getName() const
|
||||
{
|
||||
return QString::fromStdString(instance.processBinary);
|
||||
}
|
||||
|
||||
private:
|
||||
Soundux::Playback::internal::PulseAudioRecordingStream instance;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(QPulseAudioRecordingStream)
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
struct QPulseAudioRecordingStream
|
||||
{
|
||||
};
|
||||
#endif
|
||||
|
||||
struct QSound
|
||||
{
|
||||
Q_GADGET
|
||||
public:
|
||||
void setInstance(Soundux::Config::Sound instance)
|
||||
{
|
||||
this->instance = std::move(instance);
|
||||
}
|
||||
Soundux::Config::Sound getInstance() const
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
Q_INVOKABLE QString getName() const
|
||||
{
|
||||
return QString::fromStdString(instance.name);
|
||||
}
|
||||
Q_INVOKABLE QString getPath() const
|
||||
{
|
||||
return QString::fromStdString(instance.path);
|
||||
}
|
||||
Q_INVOKABLE QList<QString> getKeyBinds() const
|
||||
{
|
||||
auto hotKeys = instance.hotKeys;
|
||||
QList<QString> hotKeyStr;
|
||||
|
||||
for (auto &key : hotKeys)
|
||||
{
|
||||
hotKeyStr.push_back(QString::fromStdString(Soundux::Hooks::getKeyName(key)));
|
||||
}
|
||||
|
||||
return hotKeyStr;
|
||||
}
|
||||
|
||||
private:
|
||||
Soundux::Config::Sound instance;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(QSound)
|
||||
|
||||
struct QTab
|
||||
{
|
||||
Q_GADGET
|
||||
public:
|
||||
void setInstance(Soundux::Config::Tab instance)
|
||||
{
|
||||
this->instance = std::move(instance);
|
||||
}
|
||||
Soundux::Config::Tab getInstance() const
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
Q_INVOKABLE QString getTitle() const
|
||||
{
|
||||
return QString::fromStdString(instance.title);
|
||||
}
|
||||
Q_INVOKABLE QString getFolder() const
|
||||
{
|
||||
return QString::fromStdString(instance.folder);
|
||||
}
|
||||
Q_INVOKABLE std::vector<QSound> getSounds() const
|
||||
{
|
||||
const auto &sounds = instance.sounds;
|
||||
|
||||
std::vector<QSound> qSounds;
|
||||
for (const auto &sound : sounds)
|
||||
{
|
||||
QSound qSound;
|
||||
qSound.setInstance(sound);
|
||||
|
||||
qSounds.push_back(qSound);
|
||||
}
|
||||
return qSounds;
|
||||
}
|
||||
|
||||
private:
|
||||
Soundux::Config::Tab instance;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(QTab)
|
@ -1,222 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <json.hpp>
|
||||
#include <streambuf>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Config
|
||||
{
|
||||
using nlohmann::json;
|
||||
|
||||
struct Sound
|
||||
{
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::vector<int> hotKeys;
|
||||
std::uint64_t lastWriteTime;
|
||||
|
||||
bool operator==(const std::string &path) const
|
||||
{
|
||||
return path == this->path;
|
||||
}
|
||||
bool operator==(const Sound &other) const
|
||||
{
|
||||
return other.name == name && other.path == path;
|
||||
}
|
||||
};
|
||||
struct Tab
|
||||
{
|
||||
std::string title;
|
||||
std::string folder;
|
||||
std::vector<Sound> sounds;
|
||||
|
||||
bool operator==(const Tab &other) const
|
||||
{
|
||||
return other.folder == folder && other.title == title;
|
||||
}
|
||||
};
|
||||
struct Config
|
||||
{
|
||||
std::vector<Tab> tabs;
|
||||
|
||||
std::map<std::string, float> volumes;
|
||||
unsigned int currentOutputApplication;
|
||||
std::vector<int> stopHotKey;
|
||||
unsigned int currentTab;
|
||||
|
||||
bool allowOverlapping = false;
|
||||
bool darkTheme = true;
|
||||
bool tabHotkeysOnly;
|
||||
|
||||
int width = 995, height = 550;
|
||||
};
|
||||
|
||||
inline Config gConfig;
|
||||
|
||||
#ifdef __linux__
|
||||
inline std::string configPath = getenv("XDG_CONFIG_HOME")
|
||||
? std::string(getenv("XDG_CONFIG_HOME")) + "/Soundux/config.json"
|
||||
: std::string(getenv("HOME")) + "/.config/Soundux/config.json";
|
||||
#else
|
||||
#if _WIN32
|
||||
inline std::string configPath = []() {
|
||||
char *buffer;
|
||||
std::size_t size;
|
||||
_dupenv_s(&buffer, &size, "APPDATA");
|
||||
auto rtn = std::string(buffer) + "\\Soundux\\config.json";
|
||||
free(buffer);
|
||||
|
||||
return rtn;
|
||||
}();
|
||||
#else
|
||||
// is mac
|
||||
#endif
|
||||
#endif
|
||||
|
||||
inline void to_json(json &j, const Sound &sound)
|
||||
{
|
||||
j = json{{"name", sound.name},
|
||||
{"path", sound.path},
|
||||
{"hotKeys", sound.hotKeys},
|
||||
{"lastWriteTime", sound.lastWriteTime}};
|
||||
}
|
||||
inline void from_json(const json &j, Sound &sound)
|
||||
{
|
||||
j.at("name").get_to(sound.name);
|
||||
j.at("path").get_to(sound.path);
|
||||
j.at("hotKeys").get_to(sound.hotKeys);
|
||||
|
||||
if (j.contains("lastWriteTime"))
|
||||
{
|
||||
j.at("lastWriteTime").get_to(sound.lastWriteTime);
|
||||
}
|
||||
}
|
||||
|
||||
inline void to_json(json &j, const Tab &tab)
|
||||
{
|
||||
j = json{{"title", tab.title}, {"folder", tab.folder}, {"sounds", tab.sounds}};
|
||||
}
|
||||
inline void from_json(const json &j, Tab &tab)
|
||||
{
|
||||
j.at("title").get_to(tab.title);
|
||||
j.at("sounds").get_to(tab.sounds);
|
||||
j.at("folder").get_to(tab.folder);
|
||||
}
|
||||
|
||||
inline void to_json(json &j, const Config &config)
|
||||
{
|
||||
j = json{
|
||||
{"tabs", config.tabs},
|
||||
{"darkTheme", config.darkTheme},
|
||||
{"allowOverlapping", config.allowOverlapping},
|
||||
{"currentTab", config.currentTab},
|
||||
{"tabHotkeysOnly", config.tabHotkeysOnly},
|
||||
{"stopHotKey", config.stopHotKey},
|
||||
{"volumes", config.volumes},
|
||||
{"width", config.width},
|
||||
{"height", config.height},
|
||||
};
|
||||
}
|
||||
inline void from_json(const json &j, Config &config)
|
||||
{
|
||||
j.at("tabs").get_to(config.tabs);
|
||||
j.at("darkTheme").get_to(config.darkTheme);
|
||||
j.at("currentTab").get_to(config.currentTab);
|
||||
j.at("stopHotKey").get_to(config.stopHotKey);
|
||||
j.at("tabHotkeysOnly").get_to(config.tabHotkeysOnly);
|
||||
|
||||
if (j.contains("volumes"))
|
||||
{
|
||||
j.at("volumes").get_to(config.volumes);
|
||||
}
|
||||
if (j.contains("width"))
|
||||
{
|
||||
j.at("width").get_to(config.width);
|
||||
}
|
||||
if (j.contains("height"))
|
||||
{
|
||||
j.at("height").get_to(config.height);
|
||||
}
|
||||
if (j.contains("allowOverlapping"))
|
||||
{
|
||||
j.at("allowOverlapping").get_to(config.allowOverlapping);
|
||||
}
|
||||
}
|
||||
|
||||
inline void loadConfig()
|
||||
{
|
||||
if (!std::filesystem::exists(configPath))
|
||||
{
|
||||
std::cerr << "Config file not found"
|
||||
<< std::endl; // This is not a fatal error, maybe the config file just isn't created yet
|
||||
return;
|
||||
}
|
||||
if (!std::filesystem::is_regular_file(configPath))
|
||||
{
|
||||
std::cerr << "Config should be a file" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (std::filesystem::is_empty(configPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::fstream fileStream(configPath);
|
||||
json parsed;
|
||||
|
||||
try
|
||||
{
|
||||
parsed = json::parse(fileStream);
|
||||
}
|
||||
catch (json::parse_error &err)
|
||||
{
|
||||
std::cerr << err.what() << std::endl;
|
||||
fileStream.close();
|
||||
return;
|
||||
}
|
||||
|
||||
fileStream.close();
|
||||
|
||||
try
|
||||
{
|
||||
gConfig = parsed.get<Config>();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Faied to read config" << std::endl;
|
||||
}
|
||||
}
|
||||
inline void saveConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(configPath))
|
||||
{
|
||||
std::filesystem::create_directories(configPath.substr(0, configPath.find_last_of('/')));
|
||||
}
|
||||
|
||||
std::ofstream outStream(configPath);
|
||||
json configJson = gConfig;
|
||||
|
||||
outStream << configJson.dump() << std::endl;
|
||||
|
||||
outStream.close();
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
std::cerr << "Saving config failed: " << e.what() << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Error while saving config file" << std::endl;
|
||||
}
|
||||
}
|
||||
} // namespace Config
|
||||
} // namespace Soundux
|
103
src/core/config/config.cpp
Normal file
@ -0,0 +1,103 @@
|
||||
#include "config.hpp"
|
||||
#include "../../helper/json/bindings.hpp"
|
||||
#include <chrono>
|
||||
#include <fancy.hpp>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
const std::string Config::path = []() -> std::string {
|
||||
#if defined(__linux__)
|
||||
const auto *configPath = std::getenv("XDG_CONFIG_HOME"); // NOLINT
|
||||
if (configPath)
|
||||
{
|
||||
return std::string(configPath) + "/Soundux/config.json";
|
||||
}
|
||||
return std::string(std::getenv("HOME")) + "/.config/Soundux/config.json"; // NOLINT
|
||||
#elif defined(_WIN32)
|
||||
char *buffer;
|
||||
std::size_t size;
|
||||
_dupenv_s(&buffer, &size, "APPDATA");
|
||||
auto rtn = std::string(buffer) + "\\Soundux\\config.json";
|
||||
free(buffer);
|
||||
|
||||
return rtn;
|
||||
#endif
|
||||
}();
|
||||
|
||||
void Config::save()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
{
|
||||
std::filesystem::path configFile(path);
|
||||
std::filesystem::create_directories(configFile.parent_path());
|
||||
}
|
||||
std::ofstream configFile(path);
|
||||
configFile << nlohmann::json(*this).dump();
|
||||
configFile.close();
|
||||
Fancy::fancy.logTime().success() << "Config written" << std::endl;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to write config: " << e.what() << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to write config" << std::endl;
|
||||
}
|
||||
}
|
||||
void Config::load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "Config not found" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::fstream configFile(path);
|
||||
std::string content((std::istreambuf_iterator<char>(configFile)), std::istreambuf_iterator<char>());
|
||||
auto json = nlohmann::json::parse(content, nullptr, false);
|
||||
if (json.is_discarded())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Config seems corrupted" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
auto conf = json.get<Config>();
|
||||
data = conf.data;
|
||||
settings = conf.settings;
|
||||
Fancy::fancy.logTime().success() << "Config read" << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Fancy::fancy.logTime().warning()
|
||||
<< "Found possibly old config format, moving old config..." << std::endl;
|
||||
|
||||
std::filesystem::path configFile(path);
|
||||
std::filesystem::rename(
|
||||
path,
|
||||
configFile.parent_path() /
|
||||
("soundux_config_old_" +
|
||||
std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ".json"));
|
||||
}
|
||||
}
|
||||
configFile.close();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "Failed to read config: " << e.what() << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "Failed to read config" << std::endl;
|
||||
}
|
||||
}
|
||||
} // namespace Soundux::Objects
|
19
src/core/config/config.hpp
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "../global/objects.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
struct Config
|
||||
{
|
||||
Data data;
|
||||
Settings settings;
|
||||
|
||||
void save();
|
||||
void load();
|
||||
static const std::string path;
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
@ -1,514 +0,0 @@
|
||||
#include "core.h"
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
Core::Core(QObject *parent) : QObject(parent) {}
|
||||
|
||||
void Core::setEngine(QQmlApplicationEngine *engine)
|
||||
{
|
||||
this->engine = engine;
|
||||
}
|
||||
|
||||
void Core::loadSettings()
|
||||
{
|
||||
if (Soundux::Playback::usedDevices.find(Soundux::Playback::defaultPlayback.name) !=
|
||||
Soundux::Playback::usedDevices.end())
|
||||
{
|
||||
setLocalVolume(Soundux::Playback::usedDevices[Soundux::Playback::defaultPlayback.name]);
|
||||
}
|
||||
#ifdef __linux__
|
||||
if (Soundux::Playback::usedDevices.find(Soundux::Playback::internal::sinkName) !=
|
||||
Soundux::Playback::usedDevices.end())
|
||||
{
|
||||
setRemoteVolume(Soundux::Playback::usedDevices[Soundux::Playback::internal::sinkName]);
|
||||
}
|
||||
|
||||
auto index = Soundux::Config::gConfig.currentOutputApplication;
|
||||
auto sources = Soundux::Playback::getSources();
|
||||
if (sources.size() > index)
|
||||
{
|
||||
setOutputApplication(index);
|
||||
}
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
auto index = Soundux::Config::gConfig.currentOutputApplication;
|
||||
auto devices = Soundux::Playback::getPlaybackDevices();
|
||||
if (devices.size() > index)
|
||||
{
|
||||
auto &device = devices[index];
|
||||
if (Soundux::Playback::usedDevices.find(device.name) != Soundux::Playback::usedDevices.end())
|
||||
{
|
||||
setRemoteVolume(Soundux::Playback::usedDevices[device.name]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
setSize(Soundux::Config::gConfig.width, Soundux::Config::gConfig.height);
|
||||
}
|
||||
|
||||
void Core::refresh()
|
||||
{
|
||||
this->engine->clearComponentCache();
|
||||
this->engine->load("../src/main.qml");
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
void Core::setLinuxSink(const ma_device_info &linuxSink)
|
||||
{
|
||||
sink = linuxSink;
|
||||
}
|
||||
|
||||
std::vector<QPulseAudioRecordingStream> Core::getOutputApplications()
|
||||
{
|
||||
std::vector<QPulseAudioRecordingStream> qStreams;
|
||||
auto pulseStreams = Soundux::Playback::getSources();
|
||||
for (const auto &pulseStream : pulseStreams)
|
||||
{
|
||||
QPulseAudioRecordingStream stream;
|
||||
stream.setInstance(pulseStream);
|
||||
|
||||
qStreams.push_back(stream);
|
||||
}
|
||||
|
||||
return qStreams;
|
||||
}
|
||||
void Core::currentOutputApplicationChanged(int index)
|
||||
{
|
||||
if (index >= 0)
|
||||
{
|
||||
Soundux::Config::gConfig.currentOutputApplication = index;
|
||||
}
|
||||
}
|
||||
// Windows function that we need to define for moc
|
||||
QList<QString> Core::getPlaybackDevices()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
// Define Linux functions to prevent moc failure.
|
||||
void Core::setLinuxSink(const ma_device_info &linuxSink) {}
|
||||
std::vector<QPulseAudioRecordingStream> Core::getOutputApplications()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// Windows functions
|
||||
void Core::currentOutputApplicationChanged(int index)
|
||||
{
|
||||
Soundux::Config::gConfig.currentOutputApplication = index;
|
||||
auto devices = Soundux::Playback::getPlaybackDevices();
|
||||
if (devices.size() > (unsigned int)index)
|
||||
{
|
||||
sink = devices[index];
|
||||
}
|
||||
}
|
||||
QList<QString> Core::getPlaybackDevices()
|
||||
{
|
||||
auto devices = Soundux::Playback::getPlaybackDevices();
|
||||
QList<QString> rtn;
|
||||
for (auto &device : devices)
|
||||
{
|
||||
rtn.push_back(QString::fromStdString(device.name));
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
std::vector<QTab> Core::getTabs()
|
||||
{
|
||||
std::vector<QTab> qTabs;
|
||||
for (const auto &tab : Soundux::Config::gConfig.tabs)
|
||||
{
|
||||
QTab qTab;
|
||||
qTab.setInstance(tab);
|
||||
|
||||
qTabs.push_back(qTab);
|
||||
}
|
||||
|
||||
return qTabs;
|
||||
}
|
||||
|
||||
void Core::addFolderTab(const QUrl &folder)
|
||||
{
|
||||
Soundux::Config::Tab tab;
|
||||
tab.title = folder.fileName().toStdString();
|
||||
tab.folder = folder.path().toStdString();
|
||||
|
||||
updateFolderSounds(tab);
|
||||
|
||||
Soundux::Config::gConfig.tabs.push_back(tab);
|
||||
|
||||
Soundux::Config::saveConfig();
|
||||
emit foldersChanged();
|
||||
}
|
||||
|
||||
void Core::updateFolderSounds(const QTab &qTab)
|
||||
{
|
||||
// TODO(d3s0x): fix that the previously selected item is selected afterwards when it's still there
|
||||
auto instance = qTab.getInstance();
|
||||
for (auto &tab : Soundux::Config::gConfig.tabs)
|
||||
{
|
||||
if (instance == tab)
|
||||
{
|
||||
updateFolderSounds(tab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Soundux::Config::saveConfig();
|
||||
}
|
||||
|
||||
void Core::updateFolderSounds(Soundux::Config::Tab &tab)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Please dont ask why we have to do this. I don't know either...
|
||||
auto path = tab.folder;
|
||||
if (path[0] == '/')
|
||||
{
|
||||
path = path.substr(1);
|
||||
}
|
||||
#else
|
||||
auto path = tab.folder;
|
||||
#endif
|
||||
|
||||
if (std::filesystem::exists(path))
|
||||
{
|
||||
std::vector<Soundux::Config::Sound> newSounds;
|
||||
for (const auto &entry : std::filesystem::directory_iterator(path))
|
||||
{
|
||||
std::filesystem::path file = entry;
|
||||
if (entry.is_symlink())
|
||||
{
|
||||
file = std::filesystem::read_symlink(entry);
|
||||
if (file.has_relative_path())
|
||||
{
|
||||
file = std::filesystem::canonical(path / file);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.extension() != ".mp3" && file.extension() != ".wav" && file.extension() != ".flac")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Soundux::Config::Sound sound;
|
||||
|
||||
std::error_code ec;
|
||||
auto writeTime = std::filesystem::last_write_time(file, ec);
|
||||
if (!ec)
|
||||
{
|
||||
sound.lastWriteTime = writeTime.time_since_epoch().count();
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Failed to get last write time" << std::endl;
|
||||
}
|
||||
|
||||
sound.name = file.filename().u8string();
|
||||
sound.path = file.u8string();
|
||||
|
||||
if (auto oldSound = std::find_if(tab.sounds.begin(), tab.sounds.end(),
|
||||
[&](auto &item) { return item.path == file.u8string(); });
|
||||
oldSound != tab.sounds.end())
|
||||
{
|
||||
sound.hotKeys = oldSound->hotKeys;
|
||||
}
|
||||
|
||||
newSounds.push_back(sound);
|
||||
}
|
||||
|
||||
std::sort(newSounds.begin(), newSounds.end(),
|
||||
[](auto &first, auto &second) { return first.lastWriteTime > second.lastWriteTime; });
|
||||
|
||||
tab.sounds = newSounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
Soundux::Config::gConfig.tabs.erase(
|
||||
std::remove(Soundux::Config::gConfig.tabs.begin(), Soundux::Config::gConfig.tabs.end(), tab));
|
||||
emit foldersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Core::removeTab()
|
||||
{
|
||||
auto index = Soundux::Config::gConfig.currentTab;
|
||||
|
||||
if (Soundux::Config::gConfig.tabs.size() > index)
|
||||
{
|
||||
|
||||
Soundux::Config::gConfig.tabs.erase(Soundux::Config::gConfig.tabs.begin() + index);
|
||||
Soundux::Config::saveConfig();
|
||||
emit foldersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Core::currentTabChanged(int index)
|
||||
{
|
||||
if (index >= 0)
|
||||
{
|
||||
Soundux::Config::gConfig.currentTab = index;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<QSound> Core::getSounds()
|
||||
{
|
||||
std::vector<QSound> qSounds;
|
||||
|
||||
if (Soundux::Config::gConfig.tabs.size() > Soundux::Config::gConfig.currentTab)
|
||||
{
|
||||
for (const auto &sound : Soundux::Config::gConfig.tabs[Soundux::Config::gConfig.currentTab].sounds)
|
||||
{
|
||||
QSound qSound;
|
||||
qSound.setInstance(sound);
|
||||
|
||||
qSounds.push_back(qSound);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Soundux::Config::gConfig.currentTab = Soundux::Config::gConfig.tabs.size() - 1;
|
||||
}
|
||||
|
||||
return qSounds;
|
||||
}
|
||||
|
||||
std::vector<QSound> Core::getAllSounds()
|
||||
{
|
||||
std::vector<QSound> qSounds;
|
||||
for (auto &tab : Soundux::Config::gConfig.tabs)
|
||||
{
|
||||
for (const auto &sound : tab.sounds)
|
||||
{
|
||||
QSound qSound;
|
||||
qSound.setInstance(sound);
|
||||
|
||||
qSounds.push_back(qSound);
|
||||
}
|
||||
}
|
||||
|
||||
return qSounds;
|
||||
}
|
||||
|
||||
void Core::playSoundByPath(const QString &path)
|
||||
{
|
||||
for (const auto &tab : Soundux::Config::gConfig.tabs)
|
||||
{
|
||||
auto sound = std::find_if(tab.sounds.begin(), tab.sounds.end(),
|
||||
[&](const auto &item) { return item.path == path.toStdString(); });
|
||||
if (sound != tab.sounds.end())
|
||||
{
|
||||
playSound(*sound);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Core::playSound(int index)
|
||||
{
|
||||
if (Soundux::Config::gConfig.currentTab < Soundux::Config::gConfig.tabs.size() &&
|
||||
Soundux::Config::gConfig.tabs[Soundux::Config::gConfig.currentTab].sounds.size() > index)
|
||||
{
|
||||
playSound(Soundux::Config::gConfig.tabs[Soundux::Config::gConfig.currentTab].sounds[index]);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Invalid tab when playing audio" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Core::playSound(const Soundux::Config::Sound &sound)
|
||||
{
|
||||
if (!Soundux::Config::gConfig.allowOverlapping)
|
||||
{
|
||||
Soundux::Playback::stopAllAudio();
|
||||
}
|
||||
#ifdef __linux__
|
||||
static std::string moveBackCmd;
|
||||
|
||||
auto outputApp = Soundux::Playback::getCurrentOutputApplication();
|
||||
|
||||
if (outputApp)
|
||||
{
|
||||
emit playbackChanged(true);
|
||||
auto source = outputApp->source;
|
||||
|
||||
if (source != Soundux::Playback::internal::sinkMonitorId)
|
||||
{
|
||||
auto moveToSink = "pactl move-source-output " + std::to_string(outputApp->index) + " " +
|
||||
Soundux::Playback::internal::sinkMonitorId;
|
||||
moveBackCmd = "pactl move-source-output " + std::to_string(outputApp->index) + " " + source;
|
||||
|
||||
static_cast<void>(system(moveToSink.c_str()));
|
||||
}
|
||||
|
||||
// play on linux sink
|
||||
auto lastPlayedId = Soundux::Playback::playAudio(sound, sink);
|
||||
|
||||
Soundux::Playback::stopCallback = [=](const auto &info) {
|
||||
if (info.id == lastPlayedId)
|
||||
{
|
||||
emit playbackChanged(false);
|
||||
static_cast<void>(system(moveBackCmd.c_str()));
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
emit playbackChanged(false);
|
||||
emit invalidApplication();
|
||||
}
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
Soundux::Playback::playAudio(sound, sink);
|
||||
#endif
|
||||
// play for me on default playback device
|
||||
Soundux::Playback::playAudio(sound);
|
||||
}
|
||||
|
||||
void Core::changeLocalVolume(int volume)
|
||||
{
|
||||
Soundux::Playback::setVolume(Soundux::Playback::defaultPlayback.name, static_cast<float>(volume) / 100.F);
|
||||
}
|
||||
|
||||
void Core::changeRemoteVolume(int volume)
|
||||
{
|
||||
Soundux::Playback::setVolume(sink.name, static_cast<float>(volume) / 100.F);
|
||||
}
|
||||
|
||||
void Core::stopPlayback()
|
||||
{
|
||||
Soundux::Playback::stopAllAudio();
|
||||
}
|
||||
|
||||
QTab Core::getCurrentTab()
|
||||
{
|
||||
QTab qTab;
|
||||
if (Soundux::Config::gConfig.tabs.size() > Soundux::Config::gConfig.currentTab)
|
||||
{
|
||||
qTab.setInstance(Soundux::Config::gConfig.tabs[Soundux::Config::gConfig.currentTab]);
|
||||
}
|
||||
return qTab;
|
||||
}
|
||||
|
||||
void Core::hotkeyDialogFocusChanged(int focus)
|
||||
{
|
||||
if (focus)
|
||||
{
|
||||
Soundux::Hooks::internal::translateHotkeys = true;
|
||||
Soundux::Hooks::internal::capturedKeyList.clear();
|
||||
Soundux::Hooks::internal::capturedKeyStates.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
Soundux::Hooks::internal::translateHotkeys = false;
|
||||
Soundux::Hooks::internal::capturedKeyStates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Core::setStopHotkey()
|
||||
{
|
||||
Soundux::Hooks::internal::translateHotkeys = false;
|
||||
Soundux::Config::gConfig.stopHotKey = Soundux::Hooks::internal::capturedKeyList;
|
||||
Soundux::Config::saveConfig();
|
||||
Soundux::Hooks::internal::capturedKeyStates.clear();
|
||||
}
|
||||
|
||||
void Core::setHotkey(const QString &sound)
|
||||
{
|
||||
Soundux::Hooks::internal::translateHotkeys = false;
|
||||
|
||||
auto ¤tTab = Soundux::Config::gConfig.tabs[Soundux::Config::gConfig.currentTab];
|
||||
auto item = std::find(currentTab.sounds.begin(), currentTab.sounds.end(), sound.toStdString());
|
||||
|
||||
if (item != currentTab.sounds.end())
|
||||
{
|
||||
item->hotKeys = Soundux::Hooks::internal::capturedKeyList;
|
||||
emit updateCurrentTab();
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Cant find requested sound(" << sound.toStdString() << ")!" << std::endl;
|
||||
}
|
||||
|
||||
Soundux::Config::saveConfig();
|
||||
Soundux::Hooks::internal::capturedKeyStates.clear();
|
||||
}
|
||||
|
||||
int Core::getDarkMode()
|
||||
{
|
||||
return Soundux::Config::gConfig.darkTheme;
|
||||
}
|
||||
|
||||
void Core::onDarkModeChanged(int mode)
|
||||
{
|
||||
Soundux::Config::gConfig.darkTheme = mode;
|
||||
Soundux::Config::saveConfig();
|
||||
}
|
||||
|
||||
void Core::onTabHotkeyOnlyChanged(int state)
|
||||
{
|
||||
Soundux::Config::gConfig.tabHotkeysOnly = state;
|
||||
Soundux::Config::saveConfig();
|
||||
}
|
||||
int Core::getTabHotkeysOnly()
|
||||
{
|
||||
return Soundux::Config::gConfig.tabHotkeysOnly;
|
||||
}
|
||||
|
||||
QList<QString> Core::getStopHotKey()
|
||||
{
|
||||
QList<QString> rtn;
|
||||
for (const auto &key : Soundux::Config::gConfig.stopHotKey)
|
||||
{
|
||||
rtn.push_back(QString::fromStdString(Soundux::Hooks::getKeyName(key)));
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
QList<QString> Core::getCurrentHotKey(const QString &sound)
|
||||
{
|
||||
QList<QString> rtn;
|
||||
|
||||
auto ¤tTab = Soundux::Config::gConfig.tabs[Soundux::Config::gConfig.currentTab];
|
||||
auto item = std::find(currentTab.sounds.begin(), currentTab.sounds.end(), sound.toStdString());
|
||||
|
||||
if (item != currentTab.sounds.end())
|
||||
{
|
||||
for (const auto &key : item->hotKeys)
|
||||
{
|
||||
rtn.push_back(QString::fromStdString(Soundux::Hooks::getKeyName(key)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Cant find requested sound(" << sound.toStdString() << ")!" << std::endl;
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
int Core::isWindows()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Core::onSizeChanged(int width, int height)
|
||||
{
|
||||
Soundux::Config::gConfig.width = width;
|
||||
Soundux::Config::gConfig.height = height;
|
||||
}
|
||||
|
||||
void Core::onAllowOverlappingChanged(int state)
|
||||
{
|
||||
Soundux::Config::gConfig.allowOverlapping = state;
|
||||
Soundux::Config::saveConfig();
|
||||
}
|
||||
|
||||
int Core::getAllowOverlapping()
|
||||
{
|
||||
return Soundux::Config::gConfig.allowOverlapping;
|
||||
}
|
101
src/core/core.h
@ -1,101 +0,0 @@
|
||||
#pragma once
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <vector>
|
||||
|
||||
#include "../config/config.h"
|
||||
#include "../bindings/bindings.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "../hotkeys/windows.h"
|
||||
#include "../playback/windows.h"
|
||||
#else
|
||||
#ifdef __linux__
|
||||
#include "../hotkeys/linux.h"
|
||||
#include "../playback/linux.h"
|
||||
// #else
|
||||
// #include "../hotkeys/mac.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
class Core : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Core(QObject * = nullptr);
|
||||
|
||||
public slots:
|
||||
void setEngine(QQmlApplicationEngine *);
|
||||
void loadSettings();
|
||||
void refresh();
|
||||
|
||||
void onSizeChanged(int, int);
|
||||
|
||||
void removeTab();
|
||||
QTab getCurrentTab();
|
||||
std::vector<QTab> getTabs();
|
||||
|
||||
void addFolderTab(const QUrl &);
|
||||
void updateFolderSounds(const QTab &);
|
||||
void updateFolderSounds(Soundux::Config::Tab &);
|
||||
|
||||
void changeLocalVolume(int);
|
||||
void changeRemoteVolume(int);
|
||||
|
||||
std::vector<QSound> getAllSounds();
|
||||
std::vector<QSound> getSounds();
|
||||
|
||||
void playSound(const Soundux::Config::Sound &);
|
||||
void playSoundByPath(const QString &);
|
||||
void playSound(int);
|
||||
void stopPlayback();
|
||||
|
||||
void currentTabChanged(int);
|
||||
|
||||
void setStopHotkey();
|
||||
void setHotkey(const QString &);
|
||||
void hotkeyDialogFocusChanged(int);
|
||||
|
||||
int getDarkMode();
|
||||
void onDarkModeChanged(int);
|
||||
|
||||
int getAllowOverlapping();
|
||||
void onAllowOverlappingChanged(int);
|
||||
|
||||
void onTabHotkeyOnlyChanged(int);
|
||||
int getTabHotkeysOnly();
|
||||
|
||||
int isWindows();
|
||||
QList<QString> getStopHotKey();
|
||||
QList<QString> getCurrentHotKey(const QString &);
|
||||
|
||||
// Can't use preprocessor here because qt doesnt like that.
|
||||
|
||||
/* Linux */
|
||||
void setLinuxSink(const ma_device_info &);
|
||||
void currentOutputApplicationChanged(int);
|
||||
std::vector<QPulseAudioRecordingStream> getOutputApplications();
|
||||
/* */
|
||||
|
||||
/* Windows */
|
||||
QList<QString> getPlaybackDevices();
|
||||
/* */
|
||||
|
||||
signals:
|
||||
void keyPress(QList<QString>);
|
||||
void keyCleared();
|
||||
|
||||
void foldersChanged();
|
||||
void setSize(int, int);
|
||||
void updateCurrentTab();
|
||||
void invalidApplication();
|
||||
void setLocalVolume(float);
|
||||
void setRemoteVolume(float);
|
||||
void setOutputApplication(int);
|
||||
void playbackChanged(int);
|
||||
|
||||
private:
|
||||
QQmlApplicationEngine *engine{};
|
||||
ma_device_info sink;
|
||||
};
|
||||
|
||||
inline Core gCore;
|
32
src/core/global/globals.hpp
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "../../helper/audio/audio.hpp"
|
||||
#if defined(__linux__)
|
||||
#include "../../helper/audio/linux/pulse.hpp"
|
||||
#endif
|
||||
#include "../../helper/threads/processing.hpp"
|
||||
#include "../../ui/ui.hpp"
|
||||
#include "../config/config.hpp"
|
||||
#include "../hotkeys/hotkeys.hpp"
|
||||
#include "objects.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Globals
|
||||
{
|
||||
inline Objects::Data gData;
|
||||
inline Objects::Audio gAudio;
|
||||
#if defined(__linux__)
|
||||
inline Objects::Pulse gPulse;
|
||||
#endif
|
||||
inline Objects::Config gConfig;
|
||||
inline Objects::Hotkeys gHotKeys;
|
||||
inline Objects::Settings gSettings;
|
||||
inline std::unique_ptr<Objects::Window> gGui;
|
||||
inline Objects::ProcessingQueue<std::uintptr_t> gQueue;
|
||||
|
||||
/* Allows for fast & easy sound access, is populated on start up */
|
||||
inline std::shared_mutex gSoundsMutex;
|
||||
inline std::map<std::uint32_t, std::reference_wrapper<Objects::Sound>> gSounds;
|
||||
} // namespace Globals
|
||||
} // namespace Soundux
|
141
src/core/global/objects.cpp
Normal file
@ -0,0 +1,141 @@
|
||||
#include "objects.hpp"
|
||||
#include "globals.hpp"
|
||||
#include <algorithm>
|
||||
#include <fancy.hpp>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
Tab Data::addTab(Tab tab)
|
||||
{
|
||||
tab.id = tabs.size();
|
||||
tabs.emplace_back(tab);
|
||||
std::unique_lock lock(Globals::gSoundsMutex);
|
||||
|
||||
for (auto &sound : tabs.back().sounds)
|
||||
{
|
||||
Globals::gSounds.insert({sound.id, sound});
|
||||
}
|
||||
|
||||
return tabs.back();
|
||||
}
|
||||
void Data::removeTabById(const std::uint32_t &index)
|
||||
{
|
||||
std::unique_lock lock(Globals::gSoundsMutex);
|
||||
if (tabs.size() > index)
|
||||
{
|
||||
auto &tab = tabs.at(index);
|
||||
for (auto &sound : tab.sounds)
|
||||
{
|
||||
Globals::gSounds.erase(sound.id);
|
||||
}
|
||||
|
||||
tabs.erase(tabs.begin() + index);
|
||||
|
||||
for (int i = 0; tabs.size() > i; i++)
|
||||
{
|
||||
tabs.at(i).id = i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "Tried to remove non existant tab" << std::endl;
|
||||
}
|
||||
}
|
||||
void Data::setTabs(const std::vector<Tab> &newTabs)
|
||||
{
|
||||
tabs = newTabs;
|
||||
std::unique_lock lock(Globals::gSoundsMutex);
|
||||
|
||||
Globals::gSounds.clear();
|
||||
for (int i = 0; tabs.size() > i; i++)
|
||||
{
|
||||
auto &tab = tabs.at(i);
|
||||
tab.id = i;
|
||||
|
||||
for (auto &sound : tab.sounds)
|
||||
{
|
||||
Globals::gSounds.insert({sound.id, sound});
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<Tab> Data::getTabs() const
|
||||
{
|
||||
return tabs;
|
||||
}
|
||||
std::optional<Tab> Data::getTab(const std::uint32_t &id) const
|
||||
{
|
||||
if (tabs.size() > id)
|
||||
{
|
||||
return tabs.at(id);
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().warning() << "Tried to access non existant tab " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<std::reference_wrapper<Sound>> Data::getSound(const std::uint32_t &id)
|
||||
{
|
||||
std::shared_lock lock(Globals::gSoundsMutex);
|
||||
|
||||
if (Globals::gSounds.find(id) != Globals::gSounds.end())
|
||||
{
|
||||
return Globals::gSounds.at(id);
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().warning() << "Tried to access non existant sound " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<Tab> Data::setTab(const std::uint32_t &id, const Tab &tab)
|
||||
{
|
||||
if (tabs.size() > id)
|
||||
{
|
||||
auto &realTab = tabs.at(id);
|
||||
|
||||
std::unique_lock lock(Globals::gSoundsMutex);
|
||||
for (const auto &sound : realTab.sounds)
|
||||
{
|
||||
Globals::gSounds.erase(sound.id);
|
||||
}
|
||||
|
||||
realTab = tab;
|
||||
|
||||
for (auto &sound : realTab.sounds)
|
||||
{
|
||||
Globals::gSounds.insert({sound.id, sound});
|
||||
}
|
||||
return realTab;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().warning() << "Tried to access non existant Tab " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
Data &Data::operator=(const Data &other)
|
||||
{
|
||||
if (this == &other)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
|
||||
tabs = other.tabs;
|
||||
width = other.width;
|
||||
height = other.height;
|
||||
soundIdCounter = other.soundIdCounter;
|
||||
|
||||
std::unique_lock lock(Globals::gSoundsMutex);
|
||||
Globals::gSounds.clear();
|
||||
|
||||
for (int i = 0; tabs.size() > i; i++)
|
||||
{
|
||||
auto &tab = tabs.at(i);
|
||||
tab.id = i;
|
||||
|
||||
for (auto &sound : tab.sounds)
|
||||
{
|
||||
Globals::gSounds.insert({sound.id, sound});
|
||||
}
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
} // namespace Soundux::Objects
|
95
src/core/global/objects.hpp
Normal file
@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace nlohmann
|
||||
{
|
||||
template <typename, typename> struct adl_serializer;
|
||||
} // namespace nlohmann
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
struct AudioDevice;
|
||||
|
||||
enum class ErrorCode : std::uint8_t
|
||||
{
|
||||
FailedToPlay,
|
||||
FailedToSeek,
|
||||
FailedToPause,
|
||||
FailedToRepeat,
|
||||
FailedToResume,
|
||||
FailedToMoveToSink,
|
||||
SoundNotFound,
|
||||
FolderDoesNotExist,
|
||||
TabDoesNotExist,
|
||||
FailedToSetHotkey,
|
||||
FailedToStartPassthrough,
|
||||
FailedToMoveBack,
|
||||
FailedToMoveBackPassthrough,
|
||||
FailedToRevertDefaultSource,
|
||||
FailedToSetDefaultSource,
|
||||
};
|
||||
|
||||
struct Sound
|
||||
{
|
||||
std::uint32_t id;
|
||||
std::string name;
|
||||
std::string path;
|
||||
|
||||
std::vector<int> hotkeys;
|
||||
std::uint64_t modifiedDate;
|
||||
};
|
||||
struct Tab
|
||||
{
|
||||
std::uint32_t id; //* Equal to index
|
||||
std::string name;
|
||||
std::string path;
|
||||
|
||||
std::vector<Sound> sounds;
|
||||
};
|
||||
|
||||
struct Settings
|
||||
{
|
||||
std::vector<int> stopHotkey;
|
||||
bool useAsDefaultDevice = false;
|
||||
std::uint32_t selectedTab = 0;
|
||||
bool allowOverlapping = true;
|
||||
bool tabHotkeysOnly = false;
|
||||
bool darkTheme = true;
|
||||
bool gridView = false;
|
||||
std::string output;
|
||||
|
||||
float remoteVolume = 1.f;
|
||||
float localVolume = 1.f;
|
||||
};
|
||||
class Data
|
||||
{
|
||||
template <typename, typename> friend struct nlohmann::adl_serializer;
|
||||
|
||||
private:
|
||||
std::vector<Tab> tabs;
|
||||
|
||||
public:
|
||||
int width = 1280, height = 720;
|
||||
std::uint32_t soundIdCounter = 0;
|
||||
|
||||
std::vector<Tab> getTabs() const;
|
||||
void setTabs(const std::vector<Tab> &);
|
||||
std::optional<Tab> setTab(const std::uint32_t &, const Tab &);
|
||||
|
||||
Tab addTab(Tab);
|
||||
void removeTabById(const std::uint32_t &);
|
||||
|
||||
std::optional<Tab> getTab(const std::uint32_t &) const;
|
||||
std::optional<std::reference_wrapper<Sound>> getSound(const std::uint32_t &);
|
||||
|
||||
Data &operator=(const Data &other);
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
90
src/core/hotkeys/hotkeys.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
#include "hotkeys.hpp"
|
||||
#include "../global/globals.hpp"
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
void Hotkeys::init()
|
||||
{
|
||||
listener = std::thread([this] { listen(); });
|
||||
}
|
||||
void Hotkeys::stop()
|
||||
{
|
||||
kill = true;
|
||||
listener.join();
|
||||
}
|
||||
void Hotkeys::shouldNotify(bool status)
|
||||
{
|
||||
notify = status;
|
||||
}
|
||||
void Hotkeys::onKeyUp(int key)
|
||||
{
|
||||
if (notify && !pressedKeys.empty())
|
||||
{
|
||||
Globals::gGui->onHotKeyReceived(pressedKeys);
|
||||
pressedKeys.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
pressedKeys.erase(std::remove_if(pressedKeys.begin(), pressedKeys.end(),
|
||||
[key](const auto &item) { return key == item; }),
|
||||
pressedKeys.end());
|
||||
}
|
||||
}
|
||||
void Hotkeys::onKeyDown(int key)
|
||||
{
|
||||
pressedKeys.emplace_back(key);
|
||||
if (pressedKeys == Globals::gSettings.stopHotkey)
|
||||
{
|
||||
Globals::gGui->stopSounds();
|
||||
return;
|
||||
}
|
||||
if (Globals::gSettings.tabHotkeysOnly)
|
||||
{
|
||||
auto tab = Globals::gData.getTab(Globals::gSettings.selectedTab);
|
||||
if (tab)
|
||||
{
|
||||
auto sound = std::find_if(tab->sounds.begin(), tab->sounds.end(),
|
||||
[&](auto &item) { return item.hotkeys == pressedKeys; });
|
||||
if (sound != tab->sounds.end())
|
||||
{
|
||||
auto pSound = Globals::gGui->playSound(sound->id);
|
||||
if (pSound)
|
||||
{
|
||||
Globals::gGui->onSoundPlayed(*pSound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::shared_lock lock(Globals::gSoundsMutex);
|
||||
if (auto sound =
|
||||
std::find_if(Globals::gSounds.begin(), Globals::gSounds.end(),
|
||||
[&](const auto &item) { return item.second.get().hotkeys == pressedKeys; });
|
||||
sound != Globals::gSounds.end())
|
||||
{
|
||||
auto pSound = Globals::gGui->playSound(sound->first);
|
||||
if (pSound)
|
||||
{
|
||||
Globals::gGui->onSoundPlayed(*pSound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string Hotkeys::getKeySequence(const std::vector<int> &keys)
|
||||
{
|
||||
std::string rtn;
|
||||
for (const auto &key : keys)
|
||||
{
|
||||
rtn += getKeyName(key) + " + ";
|
||||
}
|
||||
if (!rtn.empty())
|
||||
{
|
||||
return rtn.substr(0, rtn.length() - 3);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
33
src/core/hotkeys/hotkeys.hpp
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
class Hotkeys
|
||||
{
|
||||
std::thread listener;
|
||||
std::atomic<bool> kill = false;
|
||||
std::atomic<bool> notify = false;
|
||||
|
||||
std::vector<int> pressedKeys;
|
||||
|
||||
private:
|
||||
void listen();
|
||||
|
||||
public:
|
||||
void init();
|
||||
void stop();
|
||||
void shouldNotify(bool);
|
||||
|
||||
void onKeyUp(int);
|
||||
void onKeyDown(int);
|
||||
std::string getKeyName(const int &);
|
||||
std::string getKeySequence(const std::vector<int> &);
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
107
src/core/hotkeys/linux/x11.cpp
Normal file
@ -0,0 +1,107 @@
|
||||
#if defined(__linux__) && __has_include(<X11/Xlib.h>)
|
||||
#include "../hotkeys.hpp"
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XI2.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <fancy.hpp>
|
||||
#include <thread>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
Display *display;
|
||||
void Hotkeys::listen()
|
||||
{
|
||||
auto *displayenv = std::getenv("DISPLAY"); // NOLINT
|
||||
auto *x11Display = XOpenDisplay(displayenv);
|
||||
|
||||
if (!x11Display)
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "DISPLAY is not set, defaulting to :0" << std::endl;
|
||||
if (!(x11Display = XOpenDisplay(":0")))
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Could not open X11 Display" << std::endl;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Fancy::fancy.logTime() << "Using DISPLAY " << displayenv << std::endl;
|
||||
}
|
||||
display = x11Display;
|
||||
|
||||
int major_op = 0, event_rtn = 0, ext_rtn = 0;
|
||||
if (!XQueryExtension(display, "XInputExtension", &major_op, &event_rtn, &ext_rtn))
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to find XInputExtension" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
Window root = DefaultRootWindow(display); // NOLINT
|
||||
|
||||
XIEventMask mask;
|
||||
mask.deviceid = XIAllMasterDevices;
|
||||
mask.mask_len = XIMaskLen(XI_LASTEVENT);
|
||||
mask.mask = static_cast<unsigned char *>(calloc(mask.mask_len, sizeof(char)));
|
||||
|
||||
XISetMask(mask.mask, XI_RawKeyPress);
|
||||
XISetMask(mask.mask, XI_RawKeyRelease);
|
||||
XISelectEvents(display, root, &mask, 1);
|
||||
|
||||
XSync(display, 0);
|
||||
free(mask.mask);
|
||||
|
||||
while (!kill)
|
||||
{
|
||||
if (XPending(display) != 0)
|
||||
{
|
||||
XEvent event;
|
||||
XNextEvent(display, &event);
|
||||
auto *cookie = reinterpret_cast<XGenericEventCookie *>(&event.xcookie);
|
||||
|
||||
if (XGetEventData(display, cookie) && cookie->type == GenericEvent && cookie->extension == major_op &&
|
||||
(cookie->evtype == XI_RawKeyPress || cookie->evtype == XI_RawKeyRelease))
|
||||
{
|
||||
auto *data = reinterpret_cast<XIRawEvent *>(cookie->data);
|
||||
auto key = data->detail;
|
||||
|
||||
if (cookie->evtype == XI_RawKeyPress)
|
||||
{
|
||||
onKeyDown(key);
|
||||
}
|
||||
else if (cookie->evtype == XI_RawKeyRelease)
|
||||
{
|
||||
onKeyUp(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::this_thread::sleep_for(100ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string Hotkeys::getKeyName(const int &key)
|
||||
{
|
||||
KeySym s = XkbKeycodeToKeysym(display, key, 0, 0);
|
||||
if (NoSymbol == s)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
auto *str = XKeysymToString(s);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
} // namespace Soundux::Objects
|
||||
|
||||
#endif
|
80
src/core/hotkeys/windows/windows.cpp
Normal file
@ -0,0 +1,80 @@
|
||||
#if defined(_WIN32)
|
||||
#include "../../global/globals.hpp"
|
||||
#include "../hotkeys.hpp"
|
||||
#include <Windows.h>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
HHOOK oKeyBoardProc;
|
||||
LRESULT CALLBACK keyBoardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode == HC_ACTION)
|
||||
{
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
|
||||
{
|
||||
auto *info = reinterpret_cast<PKBDLLHOOKSTRUCT>(lParam);
|
||||
Globals::gHotKeys.onKeyDown(info->vkCode);
|
||||
}
|
||||
else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
|
||||
{
|
||||
auto *info = reinterpret_cast<PKBDLLHOOKSTRUCT>(lParam);
|
||||
Globals::gHotKeys.onKeyUp(info->vkCode);
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(oKeyBoardProc, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
void Hotkeys::listen()
|
||||
{
|
||||
oKeyBoardProc = SetWindowsHookEx(WH_KEYBOARD_LL, keyBoardProc, nullptr, NULL);
|
||||
|
||||
MSG message;
|
||||
while (!kill)
|
||||
{
|
||||
if (PeekMessage(&message, nullptr, 0, 0, PM_REMOVE) != 0)
|
||||
{
|
||||
TranslateMessage(&message);
|
||||
DispatchMessage(&message);
|
||||
}
|
||||
}
|
||||
|
||||
UnhookWindowsHookEx(oKeyBoardProc);
|
||||
}
|
||||
|
||||
std::string Hotkeys::getKeyName(const int &key)
|
||||
{
|
||||
auto scanCode = MapVirtualKey(key, MAPVK_VK_TO_VSC);
|
||||
|
||||
CHAR name[128];
|
||||
int result = 0;
|
||||
switch (key)
|
||||
{
|
||||
case VK_LEFT:
|
||||
case VK_UP:
|
||||
case VK_RIGHT:
|
||||
case VK_DOWN:
|
||||
case VK_RCONTROL:
|
||||
case VK_RMENU:
|
||||
case VK_LWIN:
|
||||
case VK_RWIN:
|
||||
case VK_APPS:
|
||||
case VK_PRIOR:
|
||||
case VK_NEXT:
|
||||
case VK_END:
|
||||
case VK_HOME:
|
||||
case VK_INSERT:
|
||||
case VK_DELETE:
|
||||
case VK_DIVIDE:
|
||||
case VK_NUMLOCK:
|
||||
scanCode |= KF_EXTENDED;
|
||||
default:
|
||||
result = GetKeyNameTextA(scanCode << 16, name, 128);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
} // namespace Soundux::Objects
|
||||
#endif
|
436
src/helper/audio/audio.cpp
Normal file
@ -0,0 +1,436 @@
|
||||
#include "audio.hpp"
|
||||
#include "../../core/global/globals.hpp"
|
||||
#include "../../ui/ui.hpp"
|
||||
#include <fancy.hpp>
|
||||
#include <optional>
|
||||
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include <miniaudio.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <stringapiset.h>
|
||||
std::wstring widen(const std::string &s)
|
||||
{
|
||||
int wsz = MultiByteToWideChar(65001, 0, s.c_str(), -1, nullptr, 0);
|
||||
if (!wsz)
|
||||
return std::wstring();
|
||||
|
||||
std::wstring out(wsz, 0);
|
||||
MultiByteToWideChar(65001, 0, s.c_str(), -1, &out[0], wsz);
|
||||
out.resize(wsz - 1);
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
void Audio::setup()
|
||||
{
|
||||
refreshAudioDevices();
|
||||
}
|
||||
void Audio::destory()
|
||||
{
|
||||
stopAll();
|
||||
}
|
||||
std::optional<PlayingSound> Audio::play(const Objects::Sound &sound,
|
||||
const std::optional<Objects::AudioDevice> &playbackDevice,
|
||||
bool shouldNotReport)
|
||||
{
|
||||
auto *decoder = new ma_decoder;
|
||||
#if defined(_WIN32)
|
||||
auto res = ma_decoder_init_file_w(widen(sound.path).c_str(), nullptr, decoder);
|
||||
#else
|
||||
auto res = ma_decoder_init_file(sound.path.c_str(), nullptr, decoder);
|
||||
#endif
|
||||
if (res != MA_SUCCESS)
|
||||
{
|
||||
Fancy::fancy.logTime().logTime().failure()
|
||||
<< "Failed to create decoder from file: " << sound.path << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto *device = new ma_device;
|
||||
auto config = ma_device_config_init(ma_device_type_playback);
|
||||
auto length_in_pcm_frames = ma_decoder_get_length_in_pcm_frames(decoder);
|
||||
|
||||
config.pUserData = decoder;
|
||||
config.dataCallback = data_callback;
|
||||
config.sampleRate = decoder->outputSampleRate;
|
||||
config.playback.format = decoder->outputFormat;
|
||||
config.playback.channels = decoder->outputChannels;
|
||||
if (playbackDevice)
|
||||
{
|
||||
config.playback.pDeviceID = &playbackDevice->raw.id;
|
||||
}
|
||||
else
|
||||
{
|
||||
config.playback.pDeviceID = &defaultOutputDevice.raw.id;
|
||||
}
|
||||
|
||||
if (ma_device_init(nullptr, &config, device) != MA_SUCCESS)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to create default playback device" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (ma_device_start(device) != MA_SUCCESS)
|
||||
{
|
||||
ma_device_uninit(device);
|
||||
ma_decoder_uninit(decoder);
|
||||
Fancy::fancy.logTime().warning() << "Failed to play sound " << sound.path << std::endl;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
PlayingSound pSound;
|
||||
pSound.sound = sound;
|
||||
pSound.rawDevice = device;
|
||||
pSound.rawDecoder = decoder;
|
||||
pSound.length = length_in_pcm_frames;
|
||||
pSound.sampleRate = config.sampleRate;
|
||||
pSound.shouldNotReport = shouldNotReport;
|
||||
pSound.lengthInMs = static_cast<std::uint64_t>(static_cast<double>(pSound.length) /
|
||||
static_cast<double>(config.sampleRate) * 1000);
|
||||
pSound.id = ++playingSoundIdCounter;
|
||||
|
||||
soundsMutex.lock();
|
||||
playingSounds.insert({device, pSound});
|
||||
soundsMutex.unlock();
|
||||
|
||||
return pSound;
|
||||
}
|
||||
void Audio::stopAll()
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
for (const auto &sound : playingSounds)
|
||||
{
|
||||
lock.unlock();
|
||||
ma_device_uninit(sound.second.rawDevice);
|
||||
ma_decoder_uninit(sound.second.rawDecoder);
|
||||
Globals::gGui->onSoundFinished(sound.second);
|
||||
lock.lock();
|
||||
}
|
||||
playingSounds.clear();
|
||||
}
|
||||
bool Audio::stop(const std::uint32_t &soundId)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
auto sound = std::find_if(playingSounds.begin(), playingSounds.end(),
|
||||
[soundId](const auto &sound) { return sound.second.id == soundId; });
|
||||
|
||||
if (sound != playingSounds.end())
|
||||
{
|
||||
lock.unlock();
|
||||
ma_device_uninit(sound->second.rawDevice);
|
||||
ma_decoder_uninit(sound->second.rawDecoder);
|
||||
Globals::gGui->onSoundFinished(sound->second);
|
||||
lock.lock();
|
||||
|
||||
playingSounds.erase(sound);
|
||||
return true;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to stop sound with id " << soundId << ", sound does not exist"
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
std::optional<PlayingSound> Audio::pause(const std::uint32_t &soundId)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
auto sound = std::find_if(playingSounds.begin(), playingSounds.end(),
|
||||
[soundId](const auto &sound) { return sound.second.id == soundId; });
|
||||
|
||||
if (sound != playingSounds.end())
|
||||
{
|
||||
if (!sound->second.paused)
|
||||
{
|
||||
lock.unlock();
|
||||
ma_device_stop(sound->second.rawDevice);
|
||||
lock.lock();
|
||||
sound->second.paused = true;
|
||||
}
|
||||
return sound->second;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to pause sound with id " << soundId << ", sound does not exist"
|
||||
<< std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<PlayingSound> Audio::repeat(const std::uint32_t &soundId, bool shouldRepeat)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
auto sound = std::find_if(playingSounds.begin(), playingSounds.end(),
|
||||
[soundId](const auto &sound) { return sound.second.id == soundId; });
|
||||
|
||||
if (sound != playingSounds.end())
|
||||
{
|
||||
sound->second.repeat = shouldRepeat;
|
||||
return sound->second;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to set repeat for sound with id " << soundId
|
||||
<< ", sound does not exist" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<PlayingSound> Audio::resume(const std::uint32_t &soundId)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
auto sound = std::find_if(playingSounds.begin(), playingSounds.end(),
|
||||
[soundId](const auto &sound) { return sound.second.id == soundId; });
|
||||
|
||||
if (sound != playingSounds.end())
|
||||
{
|
||||
if (sound->second.paused)
|
||||
{
|
||||
ma_device_start(sound->second.rawDevice);
|
||||
sound->second.paused = false;
|
||||
}
|
||||
return sound->second;
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to resume sound with id " << soundId << ", sound does not exist"
|
||||
<< std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
float Audio::getVolume(const std::string &name)
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (name == sinkAudioDevice.name)
|
||||
{
|
||||
return Globals::gSettings.remoteVolume;
|
||||
}
|
||||
#endif
|
||||
std::shared_lock lock(deviceMutex);
|
||||
if (devices.find(name) != devices.end())
|
||||
{
|
||||
if (devices.at(name).isDefault)
|
||||
{
|
||||
return Globals::gSettings.localVolume;
|
||||
}
|
||||
return Globals::gSettings.remoteVolume;
|
||||
}
|
||||
return 1.f;
|
||||
}
|
||||
void Audio::onFinished(ma_device *device)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
if (playingSounds.find(device) != playingSounds.end())
|
||||
{
|
||||
auto &sound = playingSounds.at(device);
|
||||
lock.unlock();
|
||||
ma_device_uninit(sound.rawDevice);
|
||||
ma_decoder_uninit(sound.rawDecoder);
|
||||
Globals::gGui->onSoundFinished(sound);
|
||||
lock.lock();
|
||||
|
||||
playingSounds.erase(device);
|
||||
}
|
||||
else
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "Sound finished but is not playing" << std::endl;
|
||||
}
|
||||
}
|
||||
std::optional<PlayingSound> Audio::getPlayingSound(ma_device *device)
|
||||
{
|
||||
std::shared_lock lock(soundsMutex);
|
||||
if (playingSounds.find(device) != playingSounds.end())
|
||||
{
|
||||
return playingSounds.at(device);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
void Audio::onSoundProgressed(ma_device *device, std::uint64_t frames)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
if (playingSounds.find(device) != playingSounds.end())
|
||||
{
|
||||
auto &sound = playingSounds.at(device);
|
||||
sound.readFrames += frames;
|
||||
sound.buffer += frames;
|
||||
|
||||
if (sound.buffer > (sound.sampleRate / 2))
|
||||
{
|
||||
sound.readInMs = static_cast<std::uint64_t>(
|
||||
(static_cast<double>(sound.readFrames) / static_cast<double>(sound.length)) *
|
||||
static_cast<double>(sound.lengthInMs));
|
||||
Globals::gGui->onSoundProgressed(sound);
|
||||
sound.buffer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Audio::onSoundSeeked(ma_device *device, std::uint64_t frame)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
if (playingSounds.find(device) != playingSounds.end())
|
||||
{
|
||||
auto &sound = playingSounds.at(device);
|
||||
sound.shouldSeek = false;
|
||||
sound.readFrames = frame;
|
||||
sound.readInMs =
|
||||
static_cast<std::uint64_t>((static_cast<double>(frame) / static_cast<double>(sound.length)) *
|
||||
static_cast<double>(sound.lengthInMs));
|
||||
}
|
||||
}
|
||||
std::optional<PlayingSound> Audio::seek(const std::uint32_t &soundId, std::uint64_t position)
|
||||
{
|
||||
std::unique_lock lock(soundsMutex);
|
||||
auto sound = std::find_if(playingSounds.begin(), playingSounds.end(),
|
||||
[soundId](const auto &sound) { return sound.second.id == soundId; });
|
||||
if (sound != playingSounds.end())
|
||||
{
|
||||
sound->second.seekTo = static_cast<std::uint64_t>(
|
||||
(static_cast<double>(position) / static_cast<double>(sound->second.lengthInMs)) *
|
||||
static_cast<double>(sound->second.length));
|
||||
sound->second.shouldSeek = true;
|
||||
|
||||
auto rtn = sound->second;
|
||||
rtn.readFrames = rtn.seekTo;
|
||||
rtn.readInMs =
|
||||
static_cast<std::uint64_t>((static_cast<double>(rtn.seekTo) / static_cast<double>(rtn.length)) *
|
||||
static_cast<double>(rtn.lengthInMs));
|
||||
|
||||
return rtn;
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to seek sound with id " << soundId << ", sound does not exist"
|
||||
<< std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
void Audio::data_callback(ma_device *device, void *output, [[maybe_unused]] const void *input,
|
||||
std::uint32_t frameCount)
|
||||
{
|
||||
auto *decoder = reinterpret_cast<ma_decoder *>(device->pUserData);
|
||||
if (decoder == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
device->masterVolumeFactor = Globals::gAudio.getVolume(device->playback.name);
|
||||
auto readFrames = ma_decoder_read_pcm_frames(decoder, output, frameCount);
|
||||
auto sound = Globals::gAudio.getPlayingSound(device);
|
||||
if (sound)
|
||||
{
|
||||
if (sound->shouldSeek)
|
||||
{
|
||||
ma_decoder_seek_to_pcm_frame(decoder, sound->seekTo);
|
||||
Globals::gAudio.onSoundSeeked(device, sound->seekTo);
|
||||
}
|
||||
if (!sound->shouldNotReport && readFrames > 0)
|
||||
{
|
||||
Globals::gAudio.onSoundProgressed(device, readFrames);
|
||||
}
|
||||
|
||||
if (readFrames <= 0)
|
||||
{
|
||||
if (sound->repeat)
|
||||
{
|
||||
ma_decoder_seek_to_pcm_frame(decoder, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Globals::gQueue.push_unique(reinterpret_cast<std::uintptr_t>(device),
|
||||
[device] { Globals::gAudio.onFinished(device); });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<AudioDevice> Audio::fetchAudioDevices()
|
||||
{
|
||||
std::string defaultName;
|
||||
{
|
||||
ma_device device;
|
||||
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||
ma_device_init(nullptr, &deviceConfig, &device);
|
||||
|
||||
defaultName = device.playback.name;
|
||||
|
||||
ma_device_uninit(&device);
|
||||
}
|
||||
|
||||
ma_context context;
|
||||
|
||||
if (ma_context_init(nullptr, 0, nullptr, &context) != MA_SUCCESS)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to initialize context" << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
ma_device_info *pPlayBackDeviceInfos{};
|
||||
ma_uint32 deviceCount{};
|
||||
|
||||
ma_result result = ma_context_get_devices(&context, &pPlayBackDeviceInfos, &deviceCount, nullptr, nullptr);
|
||||
if (result != MA_SUCCESS)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to get playback devices!" << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<AudioDevice> playBackDevices;
|
||||
for (unsigned int i = 0; deviceCount > i; i++)
|
||||
{
|
||||
auto &rawDevice = pPlayBackDeviceInfos[i];
|
||||
|
||||
AudioDevice device;
|
||||
device.raw = rawDevice;
|
||||
device.name = rawDevice.name;
|
||||
device.isDefault = rawDevice.name == defaultName;
|
||||
|
||||
playBackDevices.emplace_back(device);
|
||||
}
|
||||
|
||||
ma_context_uninit(&context);
|
||||
|
||||
return playBackDevices;
|
||||
}
|
||||
std::vector<PlayingSound> Audio::getPlayingSounds()
|
||||
{
|
||||
std::shared_lock lock(soundsMutex);
|
||||
std::vector<PlayingSound> rtn;
|
||||
for (const auto &sound : playingSounds)
|
||||
{
|
||||
rtn.emplace_back(sound.second);
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
std::vector<AudioDevice> Audio::getAudioDevices()
|
||||
{
|
||||
std::shared_lock lock(deviceMutex);
|
||||
|
||||
std::vector<AudioDevice> rtn;
|
||||
for (const auto &device : devices)
|
||||
{
|
||||
rtn.emplace_back(device.second);
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
std::optional<std::reference_wrapper<AudioDevice>> Audio::getAudioDevice(const std::string &name)
|
||||
{
|
||||
std::shared_lock lock(deviceMutex);
|
||||
if (devices.find(name) != devices.end())
|
||||
{
|
||||
return devices.at(name);
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to receive AudioDevice with name " << name
|
||||
<< ", AudioDevice does not exist" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
void Audio::refreshAudioDevices()
|
||||
{
|
||||
auto deviceList = fetchAudioDevices();
|
||||
std::unique_lock lock(deviceMutex);
|
||||
|
||||
for (const auto &device : deviceList)
|
||||
{
|
||||
devices.insert({device.name, device});
|
||||
if (device.isDefault)
|
||||
{
|
||||
defaultOutputDevice = devices.at(device.name);
|
||||
}
|
||||
#if defined(__linux__)
|
||||
if (device.name == "soundux_sink")
|
||||
{
|
||||
sinkAudioDevice = devices.at(device.name);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} // namespace Soundux::Objects
|
89
src/helper/audio/audio.hpp
Normal file
@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
#include "../../core/global/objects.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <miniaudio.h>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
struct AudioDevice
|
||||
{
|
||||
ma_device_info raw;
|
||||
std::string name;
|
||||
bool isDefault;
|
||||
};
|
||||
struct PlayingSound
|
||||
{
|
||||
ma_device *rawDevice;
|
||||
ma_decoder *rawDecoder;
|
||||
std::uint64_t seekTo = 0;
|
||||
std::uint64_t length = 0;
|
||||
std::uint64_t readInMs = 0;
|
||||
std::uint64_t lengthInMs = 0;
|
||||
std::uint64_t readFrames = 0;
|
||||
std::uint64_t sampleRate = 0;
|
||||
bool shouldNotReport = false;
|
||||
|
||||
std::uint64_t buffer = 0;
|
||||
|
||||
std::uint32_t id;
|
||||
Sound sound;
|
||||
bool paused = false;
|
||||
bool repeat = false;
|
||||
bool shouldSeek = false;
|
||||
};
|
||||
class Audio
|
||||
{
|
||||
std::uint32_t playingSoundIdCounter;
|
||||
|
||||
std::shared_mutex soundsMutex;
|
||||
std::map<ma_device *, PlayingSound> playingSounds;
|
||||
|
||||
std::shared_mutex deviceMutex;
|
||||
std::map<std::string, AudioDevice> devices;
|
||||
|
||||
std::vector<AudioDevice> fetchAudioDevices();
|
||||
static void data_callback(ma_device *device, void *output, const void *input, std::uint32_t frameCount);
|
||||
|
||||
void onFinished(ma_device *);
|
||||
void onSoundSeeked(ma_device *, std::uint64_t);
|
||||
void onSoundProgressed(ma_device *, std::uint64_t);
|
||||
|
||||
float getVolume(const std::string &);
|
||||
std::optional<PlayingSound> getPlayingSound(ma_device *);
|
||||
|
||||
public:
|
||||
void stopAll();
|
||||
bool stop(const std::uint32_t &);
|
||||
std::optional<PlayingSound> pause(const std::uint32_t &);
|
||||
std::optional<PlayingSound> resume(const std::uint32_t &);
|
||||
std::optional<PlayingSound> repeat(const std::uint32_t &, bool);
|
||||
std::optional<PlayingSound> seek(const std::uint32_t &, std::uint64_t);
|
||||
std::optional<PlayingSound> play(const Objects::Sound &, const std::optional<AudioDevice> & = std::nullopt,
|
||||
bool = false);
|
||||
|
||||
std::vector<Objects::PlayingSound> getPlayingSounds();
|
||||
|
||||
void refreshAudioDevices();
|
||||
std::vector<AudioDevice> getAudioDevices();
|
||||
std::optional<std::reference_wrapper<AudioDevice>> getAudioDevice(const std::string &);
|
||||
|
||||
void setup();
|
||||
void destory();
|
||||
|
||||
#if defined(__linux__)
|
||||
AudioDevice sinkAudioDevice;
|
||||
#endif
|
||||
AudioDevice defaultOutputDevice;
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
417
src/helper/audio/linux/pulse.cpp
Normal file
@ -0,0 +1,417 @@
|
||||
#if defined(__linux__)
|
||||
#include "pulse.hpp"
|
||||
#include "fancy.hpp"
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
auto exec(const std::string &command)
|
||||
{
|
||||
std::array<char, 128> buffer;
|
||||
std::string result;
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(command.c_str(), "r"), pclose);
|
||||
if (!pipe)
|
||||
{
|
||||
throw std::runtime_error("popen failed");
|
||||
}
|
||||
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
|
||||
{
|
||||
result += buffer.data();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
auto splitByNewLine(const std::string &str)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
std::stringstream ss(str);
|
||||
for (std::string line; std::getline(ss, line, '\n');)
|
||||
{
|
||||
result.emplace_back(line);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
void Pulse::setup()
|
||||
{
|
||||
unloadLeftOverModules();
|
||||
refreshPlaybackStreams();
|
||||
refreshRecordingStreams();
|
||||
fetchDefaultPulseSource();
|
||||
|
||||
auto moduleId = exec("pactl load-module module-null-sink sink_name=soundux_sink rate=44100 "
|
||||
"sink_properties=device.description=soundux_sink");
|
||||
moduleId.erase(std::remove(moduleId.begin(), moduleId.end(), '\n'), moduleId.end());
|
||||
data.nullSinkModuleId = std::stoi(moduleId);
|
||||
|
||||
auto loopbackId = exec("pactl load-module module-loopback rate=44100 source=" + data.pulseDefaultSource +
|
||||
" sink=soundux_sink"); // NOLINT
|
||||
loopbackId.erase(std::remove(loopbackId.begin(), loopbackId.end(), '\n'), loopbackId.end());
|
||||
data.loopbackModuleId = std::stoi(loopbackId);
|
||||
|
||||
auto passThroughId = exec("pactl load-module module-null-sink sink_name=soundux_sink_passthrough rate=44100 "
|
||||
"sink_properties=device.description=soundux_sink_passthrough");
|
||||
passThroughId.erase(std::remove(passThroughId.begin(), passThroughId.end(), '\n'), passThroughId.end());
|
||||
data.passthroughModuleId = std::stoi(passThroughId);
|
||||
|
||||
auto passThroughSink = exec("pactl load-module module-loopback latency_msec=1 "
|
||||
"source=soundux_sink_passthrough.monitor sink=soundux_sink");
|
||||
passThroughSink.erase(std::remove(passThroughSink.begin(), passThroughSink.end(), '\n'), passThroughSink.end());
|
||||
data.passthroughLoopbackSinkModuleId = std::stoi(passThroughSink);
|
||||
|
||||
auto passThroughLoopback = exec("pactl load-module module-loopback source=soundux_sink_passthrough.monitor");
|
||||
passThroughLoopback.erase(std::remove(passThroughLoopback.begin(), passThroughLoopback.end(), '\n'),
|
||||
passThroughLoopback.end());
|
||||
data.passthroughLoopbackMonitorModuleId = std::stoi(passThroughLoopback);
|
||||
|
||||
refreshPlaybackStreams(true);
|
||||
refreshRecordingStreams(true);
|
||||
|
||||
static const std::regex sourceRegex(R"rgx((.*#(\d+))$|(Name: (.+)))rgx");
|
||||
auto sources = exec("LC_ALL=C pactl list sources");
|
||||
std::smatch match;
|
||||
|
||||
std::string deviceId;
|
||||
for (const auto &line : splitByNewLine(sources))
|
||||
{
|
||||
if (std::regex_search(line, match, sourceRegex))
|
||||
{
|
||||
if (match[2].matched)
|
||||
{
|
||||
deviceId = match[2];
|
||||
}
|
||||
|
||||
if (match[4].matched && match[4] == "soundux_sink.monitor")
|
||||
{
|
||||
data.sinkMonitorId = std::stoi(deviceId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to find monitor of soundux sink!" << std::endl;
|
||||
}
|
||||
void Pulse::destroy()
|
||||
{
|
||||
moveBackCurrentApplication();
|
||||
moveBackApplicationFromPassthrough();
|
||||
revertDefaultSourceToOriginal();
|
||||
|
||||
if (data.loopbackModuleId != 0)
|
||||
system(("pactl unload-module " + std::to_string(data.loopbackModuleId)).c_str()); // NOLINT
|
||||
if (data.nullSinkModuleId != 0)
|
||||
system(("pactl unload-module " + std::to_string(data.nullSinkModuleId)).c_str()); // NOLINT
|
||||
|
||||
if (data.passthroughModuleId != 0)
|
||||
system(("pactl unload-module " + std::to_string(data.passthroughModuleId)).c_str()); // NOLINT
|
||||
if (data.passthroughLoopbackSinkModuleId != 0)
|
||||
// NOLINTNEXTLINE
|
||||
system(("pactl unload-module " + std::to_string(data.passthroughLoopbackSinkModuleId)).c_str());
|
||||
if (data.passthroughLoopbackMonitorModuleId != 0)
|
||||
// NOLINTNEXTLINE
|
||||
system(("pactl unload-module " + std::to_string(data.passthroughLoopbackMonitorModuleId)).c_str());
|
||||
}
|
||||
void Pulse::fetchDefaultPulseSource()
|
||||
{
|
||||
auto info = exec("LC_ALL=C pactl info");
|
||||
static const std::regex defaultDeviceRegex("^Default Source: (.+)$");
|
||||
|
||||
std::smatch match;
|
||||
for (const auto &line : splitByNewLine(info))
|
||||
{
|
||||
if (std::regex_match(line, match, defaultDeviceRegex))
|
||||
{
|
||||
if (match[1].matched)
|
||||
{
|
||||
if (match[1].str() == "soundux_sink.monitor")
|
||||
{
|
||||
Fancy::fancy.logTime().warning()
|
||||
<< "Default Source is Soundux Sink, this should not happen!" << std::endl;
|
||||
return;
|
||||
}
|
||||
data.pulseDefaultSource = match[1];
|
||||
Fancy::fancy.logTime()
|
||||
<< "Default Pulse Source was saved: " << data.pulseDefaultSource << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bool Pulse::revertDefaultSourceToOriginal() const
|
||||
{
|
||||
if (!data.pulseDefaultSource.empty())
|
||||
{
|
||||
if (system(("pactl set-default-source " + data.pulseDefaultSource + " &>/dev/null").c_str()) != 0) // NOLINT
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Fancy::fancy.logTime().failure()
|
||||
<< "Failed to revert default source, default source was not set!" << std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Pulse::setDefaultSourceToSoundboardSink()
|
||||
{
|
||||
return system("pactl set-default-source soundux_sink.monitor &>/dev/null") == 0; // NOLINT
|
||||
}
|
||||
bool Pulse::moveApplicationToSinkMonitor(const std::string &streamName)
|
||||
{
|
||||
moveBackCurrentApplication();
|
||||
refreshRecordingStreams();
|
||||
|
||||
std::shared_lock lock(recordingStreamMutex);
|
||||
if (recordingStreams.find(streamName) != recordingStreams.end())
|
||||
{
|
||||
auto &stream = recordingStreams.at(streamName);
|
||||
currentApplication = stream;
|
||||
// NOLINTNEXTLINE
|
||||
return system(
|
||||
("pactl move-source-output " + std::to_string(stream.id) + " soundux_sink.monitor &>/dev/null")
|
||||
.c_str()) == 0;
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to find PulseRecordingStream with name: " << streamName
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
bool Pulse::moveBackCurrentApplication()
|
||||
{
|
||||
if (currentApplication)
|
||||
{
|
||||
// NOLINTNEXTLINE
|
||||
return system(("pactl move-source-output " + std::to_string(currentApplication->id) + " " +
|
||||
currentApplication->source + " &>/dev/null")
|
||||
.c_str()) == 0;
|
||||
}
|
||||
return true; //* Not having anything to moveback should count as a failure
|
||||
}
|
||||
void Pulse::refreshRecordingStreams(const bool &fix)
|
||||
{
|
||||
auto sourceList = exec("LC_ALL=C pactl list source-outputs");
|
||||
|
||||
std::vector<PulseRecordingStream> fetchedStreams;
|
||||
static const auto recordingStreamRegex = std::regex(
|
||||
R"rgx((.*#(\d+))|(Driver: (.+))|(Source: (\d+))|(.*process.*binary.* = "(.+)")|(Resample method: (.+)))rgx");
|
||||
|
||||
PulseRecordingStream stream;
|
||||
std::smatch match;
|
||||
|
||||
for (const auto &line : splitByNewLine(sourceList))
|
||||
{
|
||||
if (std::regex_search(line, match, recordingStreamRegex))
|
||||
{
|
||||
if (match[2].matched)
|
||||
{
|
||||
if (stream)
|
||||
{
|
||||
recordingStreamMutex.lock_shared();
|
||||
if (recordingStreams.find(stream.name) != recordingStreams.end())
|
||||
{
|
||||
stream.source = recordingStreams.at(stream.name).source;
|
||||
if (fix)
|
||||
{
|
||||
// NOLINTNEXTLINE
|
||||
system(("pactl move-source-output " + std::to_string(stream.id) + " " + stream.source)
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
recordingStreamMutex.unlock_shared();
|
||||
fetchedStreams.emplace_back(stream);
|
||||
}
|
||||
|
||||
stream = {};
|
||||
stream.id = std::stoi(match[2]);
|
||||
}
|
||||
else if (match[4].matched)
|
||||
{
|
||||
stream.driver = match[4];
|
||||
}
|
||||
else if (match[6].matched)
|
||||
{
|
||||
stream.source = match[6];
|
||||
}
|
||||
else if (match[8].matched)
|
||||
{
|
||||
stream.name = match[8];
|
||||
}
|
||||
else if (match[10].matched)
|
||||
{
|
||||
stream.resampleMethod = match[10];
|
||||
}
|
||||
}
|
||||
}
|
||||
recordingStreamMutex.lock_shared();
|
||||
if (stream)
|
||||
{
|
||||
if (recordingStreams.find(stream.name) != recordingStreams.end())
|
||||
{
|
||||
stream.source = recordingStreams.at(stream.name).source;
|
||||
}
|
||||
fetchedStreams.emplace_back(stream);
|
||||
}
|
||||
recordingStreamMutex.unlock_shared();
|
||||
|
||||
std::unique_lock lock(recordingStreamMutex);
|
||||
recordingStreams.clear();
|
||||
for (const auto &stream : fetchedStreams)
|
||||
{
|
||||
recordingStreams.insert({stream.name, stream});
|
||||
}
|
||||
}
|
||||
void Pulse::refreshPlaybackStreams(const bool &fix)
|
||||
{
|
||||
auto sourceList = exec("LC_ALL=C pactl list sink-inputs");
|
||||
|
||||
std::vector<PulsePlaybackStream> fetchedStreams;
|
||||
static const auto playbackStreamRegex =
|
||||
std::regex(R"rgx((.*#(\d+))|(Driver: (.+))|(Sink: (\d+))|(.*application\.name.* = "(.+)"))rgx");
|
||||
|
||||
PulsePlaybackStream stream;
|
||||
std::smatch match;
|
||||
|
||||
for (const auto &line : splitByNewLine(sourceList))
|
||||
{
|
||||
if (std::regex_search(line, match, playbackStreamRegex))
|
||||
{
|
||||
if (match[2].matched)
|
||||
{
|
||||
if (stream)
|
||||
{
|
||||
playbackStreamMutex.lock_shared();
|
||||
if (playbackStreams.find(stream.name) != playbackStreams.end())
|
||||
{
|
||||
stream.sink = playbackStreams.at(stream.name).sink;
|
||||
if (fix)
|
||||
{
|
||||
// NOLINTNEXTLINE
|
||||
system(("pactl move-sink-input " + std::to_string(stream.id) + " " + stream.sink +
|
||||
" &>/dev/null")
|
||||
.c_str());
|
||||
}
|
||||
}
|
||||
playbackStreamMutex.unlock_shared();
|
||||
fetchedStreams.emplace_back(stream);
|
||||
}
|
||||
|
||||
stream = {};
|
||||
stream.id = std::stoi(match[2]);
|
||||
}
|
||||
else if (match[4].matched)
|
||||
{
|
||||
stream.driver = match[4];
|
||||
}
|
||||
else if (match[6].matched)
|
||||
{
|
||||
stream.sink = match[6];
|
||||
}
|
||||
else if (match[8].matched)
|
||||
{
|
||||
stream.name = match[8];
|
||||
}
|
||||
}
|
||||
}
|
||||
playbackStreamMutex.lock_shared();
|
||||
if (stream)
|
||||
{
|
||||
if (playbackStreams.find(stream.name) != playbackStreams.end())
|
||||
{
|
||||
stream.sink = playbackStreams.at(stream.name).sink;
|
||||
}
|
||||
fetchedStreams.emplace_back(stream);
|
||||
}
|
||||
playbackStreamMutex.unlock_shared();
|
||||
|
||||
std::unique_lock lock(playbackStreamMutex);
|
||||
playbackStreams.clear();
|
||||
for (const auto &stream : fetchedStreams)
|
||||
{
|
||||
playbackStreams.insert({stream.name, stream});
|
||||
}
|
||||
}
|
||||
void Pulse::unloadLeftOverModules()
|
||||
{
|
||||
auto loadedModules = exec("LC_ALL=C pactl list modules");
|
||||
static const std::regex moduleRegex(R"rgx((Module #(\d+))|(Argument: .*(soundux_sink).*))rgx");
|
||||
|
||||
std::smatch match;
|
||||
std::string currentModuleId;
|
||||
for (const auto &line : splitByNewLine(loadedModules))
|
||||
{
|
||||
if (std::regex_search(line, match, moduleRegex))
|
||||
{
|
||||
if (match[2].matched)
|
||||
{
|
||||
currentModuleId = match[2];
|
||||
}
|
||||
else if (match[4].matched)
|
||||
{
|
||||
system(("pactl unload-module " + currentModuleId).c_str()); // NOLINT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<PulseRecordingStream> Pulse::getRecordingStreams()
|
||||
{
|
||||
std::shared_lock lock(recordingStreamMutex);
|
||||
std::vector<PulseRecordingStream> rtn;
|
||||
for (const auto &stream : recordingStreams)
|
||||
{
|
||||
rtn.emplace_back(stream.second);
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
std::vector<PulsePlaybackStream> Pulse::getPlaybackStreams()
|
||||
{
|
||||
std::shared_lock lock(playbackStreamMutex);
|
||||
std::vector<PulsePlaybackStream> rtn;
|
||||
for (const auto &stream : playbackStreams)
|
||||
{
|
||||
rtn.emplace_back(stream.second);
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
bool Pulse::moveBackApplicationFromPassthrough()
|
||||
{
|
||||
if (currentApplicationPassthrough)
|
||||
{
|
||||
// NOLINTNEXTLINE
|
||||
return system(("pactl move-sink-input " + std::to_string(currentApplicationPassthrough->id) + " " +
|
||||
currentApplicationPassthrough->sink + " &>/dev/null")
|
||||
.c_str()) == 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::optional<PulsePlaybackStream> Pulse::moveApplicationToApplicationPassthrough(const std::string &name)
|
||||
{
|
||||
moveBackApplicationFromPassthrough();
|
||||
|
||||
std::shared_lock lock(playbackStreamMutex);
|
||||
if (playbackStreams.find(name) != playbackStreams.end())
|
||||
{
|
||||
auto &stream = playbackStreams.at(name);
|
||||
currentApplicationPassthrough = stream;
|
||||
// NOLINTNEXTLINE
|
||||
system(("pactl move-sink-input " + std::to_string(stream.id) + " soundux_sink_passthrough &>/dev/null")
|
||||
.c_str());
|
||||
return *currentApplicationPassthrough;
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to find PulsePlaybackStream with name: " << name << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
bool Pulse::isSwitchOnConnectLoaded()
|
||||
{
|
||||
return (system("pactl list modules | grep module-switch-on-connect &>/dev/null") == 0); // NOLINT
|
||||
}
|
||||
void Pulse::unloadSwitchOnConnect()
|
||||
{
|
||||
system("pactl unload-module module-switch-on-connect &>/dev/null"); // NOLINT
|
||||
}
|
||||
bool Pulse::currentlyPassingthrough()
|
||||
{
|
||||
return currentApplicationPassthrough.has_value();
|
||||
}
|
||||
} // namespace Soundux::Objects
|
||||
#endif
|
90
src/helper/audio/linux/pulse.hpp
Normal file
@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
#if defined(__linux__)
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
struct PulseRecordingStream
|
||||
{
|
||||
std::uint32_t id;
|
||||
std::string name;
|
||||
std::string driver;
|
||||
std::string source;
|
||||
std::string resampleMethod;
|
||||
|
||||
operator bool() const
|
||||
{
|
||||
return driver == "protocol-native.c" && resampleMethod != "peaks";
|
||||
}
|
||||
};
|
||||
struct PulsePlaybackStream
|
||||
{
|
||||
std::uint32_t id;
|
||||
std::string sink;
|
||||
std::string name;
|
||||
std::string driver;
|
||||
|
||||
operator bool() const
|
||||
{
|
||||
return driver == "protocol-native.c";
|
||||
}
|
||||
};
|
||||
struct PulseData
|
||||
{
|
||||
std::uint32_t sinkMonitorId;
|
||||
std::string pulseDefaultSource;
|
||||
std::uint32_t nullSinkModuleId = 0;
|
||||
std::uint32_t loopbackModuleId = 0;
|
||||
|
||||
std::uint32_t passthroughModuleId = 0;
|
||||
std::uint32_t passthroughLoopbackSinkModuleId = 0;
|
||||
std::uint32_t passthroughLoopbackMonitorModuleId = 0;
|
||||
};
|
||||
class Pulse
|
||||
{
|
||||
void fixLeftOvers();
|
||||
void unloadLeftOverModules();
|
||||
void fetchDefaultPulseSource();
|
||||
|
||||
PulseData data;
|
||||
std::optional<PulseRecordingStream> currentApplication;
|
||||
std::optional<PulsePlaybackStream> currentApplicationPassthrough;
|
||||
|
||||
std::shared_mutex recordingStreamMutex;
|
||||
std::map<std::string, PulseRecordingStream> recordingStreams;
|
||||
|
||||
std::shared_mutex playbackStreamMutex;
|
||||
std::map<std::string, PulsePlaybackStream> playbackStreams;
|
||||
|
||||
public:
|
||||
void setup();
|
||||
void destroy();
|
||||
|
||||
void unloadSwitchOnConnect();
|
||||
bool isSwitchOnConnectLoaded();
|
||||
|
||||
bool moveBackCurrentApplication();
|
||||
bool setDefaultSourceToSoundboardSink();
|
||||
bool revertDefaultSourceToOriginal() const;
|
||||
bool moveApplicationToSinkMonitor(const std::string &);
|
||||
|
||||
void refreshRecordingStreams(const bool &fix = false);
|
||||
std::vector<PulseRecordingStream> getRecordingStreams();
|
||||
|
||||
void refreshPlaybackStreams(const bool &fix = false);
|
||||
std::vector<PulsePlaybackStream> getPlaybackStreams();
|
||||
|
||||
bool currentlyPassingthrough();
|
||||
bool moveBackApplicationFromPassthrough();
|
||||
std::optional<PulsePlaybackStream> moveApplicationToApplicationPassthrough(const std::string &);
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
||||
#endif
|
38
src/helper/exceptions/crashhandler.cpp
Normal file
@ -0,0 +1,38 @@
|
||||
#include "crashhandler.hpp"
|
||||
#include <exception>
|
||||
#include <fancy.hpp>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <csignal>
|
||||
#endif
|
||||
|
||||
void CrashHandler::init()
|
||||
{
|
||||
std::set_terminate(terminateCallback);
|
||||
#if defined(__linux__)
|
||||
signal(SIGSEGV, signalHandler);
|
||||
signal(SIGABRT, signalHandler);
|
||||
signal(SIGFPE, signalHandler);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CrashHandler::terminateCallback()
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "An exception crashed the program" << std::endl;
|
||||
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(std::current_exception());
|
||||
}
|
||||
catch (const std::exception &exception)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Exception: " >> exception.what() << std::endl;
|
||||
Fancy::fancy.logTime().failure() << "Exception Type: " >> typeid(exception).name() << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Exception: " >> "unknown" << std::endl;
|
||||
}
|
||||
|
||||
backtrace();
|
||||
}
|
15
src/helper/exceptions/crashhandler.hpp
Normal file
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
class CrashHandler
|
||||
{
|
||||
private:
|
||||
static void terminateCallback();
|
||||
static void backtrace();
|
||||
|
||||
#if defined(__linux__)
|
||||
static void signalHandler(int);
|
||||
#endif
|
||||
|
||||
public:
|
||||
static void init();
|
||||
};
|
35
src/helper/exceptions/linux/linux.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#if defined(__linux__)
|
||||
#include "../crashhandler.hpp"
|
||||
#include <csignal>
|
||||
#include <execinfo.h>
|
||||
#include <fancy.hpp>
|
||||
|
||||
void CrashHandler::backtrace()
|
||||
{
|
||||
Fancy::fancy.logTime().success() << "Backtrace available!" << std::endl;
|
||||
|
||||
void *elements[20];
|
||||
auto size = ::backtrace(elements, 20);
|
||||
auto *stack = backtrace_symbols(elements, size);
|
||||
|
||||
for (int i = 0; size > i; i++)
|
||||
{
|
||||
Fancy::fancy.logTime() << stack[i] << std::endl;
|
||||
}
|
||||
|
||||
free(stack);
|
||||
}
|
||||
|
||||
void CrashHandler::signalHandler(int signal)
|
||||
{
|
||||
if (signal == SIGFPE)
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "This crash is probably related to a bad pulseaudio config!" << std::endl;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Received Signal: " << signal << std::endl;
|
||||
|
||||
backtrace();
|
||||
exit(1); // NOLINT
|
||||
}
|
||||
#endif
|
9
src/helper/exceptions/windows/windows.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#if defined(_WIN32)
|
||||
#include "../crashhandler.hpp"
|
||||
#include <fancy.hpp>
|
||||
|
||||
void CrashHandler::backtrace()
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Backtrace is not available on Windows" << std::endl;
|
||||
}
|
||||
#endif
|
170
src/helper/json/bindings.hpp
Normal file
@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
#include "../../core/global/globals.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace nlohmann
|
||||
{
|
||||
template <> struct adl_serializer<Soundux::Objects::Sound>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::Sound &obj)
|
||||
{
|
||||
j = {{"name", obj.name},
|
||||
{"hotkeys", obj.hotkeys},
|
||||
{"hotkeySequence",
|
||||
Soundux::Globals::gHotKeys.getKeySequence(obj.hotkeys)}, //* For frontend and config readability
|
||||
{"id", obj.id},
|
||||
{"path", obj.path},
|
||||
{"modifiedDate", obj.modifiedDate}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::Sound &obj)
|
||||
{
|
||||
j.at("name").get_to(obj.name);
|
||||
j.at("hotkeys").get_to(obj.hotkeys);
|
||||
j.at("id").get_to(obj.id);
|
||||
j.at("path").get_to(obj.path);
|
||||
j.at("modifiedDate").get_to(obj.modifiedDate);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::AudioDevice>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::AudioDevice &obj)
|
||||
{
|
||||
j = {{"name", obj.name}, {"isDefault", obj.isDefault}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::AudioDevice &obj)
|
||||
{
|
||||
j.at("name").get_to(obj.name);
|
||||
j.at("isDefault").get_to(obj.isDefault);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::PlayingSound>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::PlayingSound &obj)
|
||||
{
|
||||
j = {
|
||||
{"sound", obj.sound}, {"id", obj.id},
|
||||
{"length", obj.length}, {"paused", obj.paused},
|
||||
{"lengthInMs", obj.lengthInMs}, {"repeat", obj.repeat},
|
||||
{"readFrames", obj.readFrames}, {"readInMs", obj.readInMs},
|
||||
};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::PlayingSound &obj)
|
||||
{
|
||||
j.at("sound").get_to(obj.sound);
|
||||
j.at("paused").get_to(obj.paused);
|
||||
j.at("repeat").get_to(obj.repeat);
|
||||
j.at("id").get_to(obj.id);
|
||||
j.at("length").get_to(obj.length);
|
||||
j.at("lengthInMs").get_to(obj.lengthInMs);
|
||||
j.at("readFrames").get_to(obj.readFrames);
|
||||
j.at("readInMs").get_to(obj.readInMs);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::Settings>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::Settings &obj)
|
||||
{
|
||||
j = {{"allowOverlapping", obj.allowOverlapping},
|
||||
{"output", obj.output},
|
||||
{"gridView", obj.gridView},
|
||||
{"darkTheme", obj.darkTheme},
|
||||
{"stopHotkey", obj.stopHotkey},
|
||||
{"selectedTab", obj.selectedTab},
|
||||
{"tabHotkeysOnly", obj.tabHotkeysOnly},
|
||||
{"remoteVolume", obj.remoteVolume},
|
||||
{"useAsDefaultDevice", obj.useAsDefaultDevice},
|
||||
{"localVolume", obj.localVolume}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::Settings &obj)
|
||||
{
|
||||
j.at("allowOverlapping").get_to(obj.allowOverlapping);
|
||||
j.at("output").get_to(obj.output);
|
||||
j.at("gridView").get_to(obj.gridView);
|
||||
j.at("darkTheme").get_to(obj.darkTheme);
|
||||
j.at("stopHotkey").get_to(obj.stopHotkey);
|
||||
j.at("localVolume").get_to(obj.localVolume);
|
||||
j.at("selectedTab").get_to(obj.selectedTab);
|
||||
j.at("remoteVolume").get_to(obj.remoteVolume);
|
||||
j.at("tabHotkeysOnly").get_to(obj.tabHotkeysOnly);
|
||||
j.at("useAsDefaultDevice").get_to(obj.useAsDefaultDevice);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::Tab>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::Tab &obj)
|
||||
{
|
||||
j = {{"id", obj.id}, {"name", obj.name}, {"path", obj.path}, {"sounds", obj.sounds}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::Tab &obj)
|
||||
{
|
||||
j.at("id").get_to(obj.id);
|
||||
j.at("name").get_to(obj.name);
|
||||
j.at("path").get_to(obj.path);
|
||||
j.at("sounds").get_to(obj.sounds);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::Data>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::Data &obj)
|
||||
{
|
||||
j = {{"height", obj.height},
|
||||
{"width", obj.width},
|
||||
{"tabs", obj.tabs},
|
||||
{"soundIdCounter", obj.soundIdCounter}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::Data &obj)
|
||||
{
|
||||
j.at("soundIdCounter").get_to(obj.soundIdCounter);
|
||||
j.at("height").get_to(obj.height);
|
||||
j.at("width").get_to(obj.width);
|
||||
j.at("tabs").get_to(obj.tabs);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::Config>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::Config &obj)
|
||||
{
|
||||
j = {{"data", obj.data}, {"settings", obj.settings}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::Config &obj)
|
||||
{
|
||||
j.at("data").get_to(obj.data);
|
||||
j.at("settings").get_to(obj.settings);
|
||||
}
|
||||
};
|
||||
#if defined(__linux__)
|
||||
template <> struct adl_serializer<Soundux::Objects::PulseRecordingStream>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::PulseRecordingStream &obj)
|
||||
{
|
||||
j = {{"id", obj.id},
|
||||
{"name", obj.name},
|
||||
{"driver", obj.driver},
|
||||
{"source", obj.source},
|
||||
{"resampleMethod", obj.resampleMethod}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::PulseRecordingStream &obj)
|
||||
{
|
||||
j.at("id").get_to(obj.id);
|
||||
j.at("name").get_to(obj.name);
|
||||
j.at("driver").get_to(obj.driver);
|
||||
j.at("source").get_to(obj.source);
|
||||
j.at("resampleMethod").get_to(obj.resampleMethod);
|
||||
}
|
||||
};
|
||||
template <> struct adl_serializer<Soundux::Objects::PulsePlaybackStream>
|
||||
{
|
||||
static void to_json(json &j, const Soundux::Objects::PulsePlaybackStream &obj)
|
||||
{
|
||||
j = {{"id", obj.id}, {"name", obj.name}, {"sink", obj.sink}, {"driver", obj.driver}};
|
||||
}
|
||||
static void from_json(const json &j, Soundux::Objects::PulsePlaybackStream &obj)
|
||||
{
|
||||
j.at("id").get_to(obj.id);
|
||||
j.at("name").get_to(obj.name);
|
||||
j.at("sink").get_to(obj.sink);
|
||||
j.at("driver").get_to(obj.driver);
|
||||
}
|
||||
};
|
||||
#endif
|
||||
} // namespace nlohmann
|
114
src/helper/threads/processing.hpp
Normal file
@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
#include <shared_mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
template <typename UID = int> class ProcessingQueue
|
||||
{
|
||||
struct Item
|
||||
{
|
||||
std::shared_ptr<std::atomic<bool>> handled;
|
||||
std::function<void()> callback;
|
||||
UID identifier;
|
||||
};
|
||||
|
||||
private:
|
||||
std::queue<Item> queue;
|
||||
std::mutex queueMutex;
|
||||
|
||||
std::vector<UID> unhandled;
|
||||
std::shared_mutex unhandledMutex;
|
||||
|
||||
std::condition_variable cv;
|
||||
std::atomic<bool> stop;
|
||||
std::thread handler;
|
||||
|
||||
void handle()
|
||||
{
|
||||
std::unique_lock lock(queueMutex);
|
||||
while (!stop)
|
||||
{
|
||||
cv.wait(lock, [&]() { return !queue.empty() || stop; });
|
||||
while (!queue.empty())
|
||||
{
|
||||
auto front = std::move(queue.front());
|
||||
queue.pop();
|
||||
|
||||
lock.unlock();
|
||||
|
||||
front.callback();
|
||||
if (front.handled)
|
||||
{
|
||||
front.handled->store(true);
|
||||
}
|
||||
auto found = std::find(unhandled.begin(), unhandled.end(), front.identifier);
|
||||
if (found != unhandled.end())
|
||||
{
|
||||
unhandled.erase(found);
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void push(const std::function<void()> &item)
|
||||
{
|
||||
std::unique_lock lock(queueMutex);
|
||||
queue.emplace({nullptr, item});
|
||||
lock.unlock();
|
||||
cv.notify_one();
|
||||
}
|
||||
void push_unique(const UID &identifier, const std::function<void()> &item)
|
||||
{
|
||||
{
|
||||
std::shared_lock lock(unhandledMutex);
|
||||
if (std::find(unhandled.begin(), unhandled.end(), identifier) != unhandled.end())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock lock(queueMutex);
|
||||
std::unique_lock lock_(unhandledMutex);
|
||||
|
||||
unhandled.emplace_back(identifier);
|
||||
|
||||
queue.push({nullptr, item, identifier});
|
||||
lock.unlock();
|
||||
cv.notify_one();
|
||||
}
|
||||
void wait(const std::function<void()> &item)
|
||||
{
|
||||
std::unique_lock lock(queueMutex);
|
||||
auto status = std::make_shared<std::atomic<bool>>();
|
||||
queue.emplace({status, item});
|
||||
lock.unlock();
|
||||
cv.notify_one();
|
||||
|
||||
while (!*status)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
ProcessingQueue()
|
||||
{
|
||||
handler = std::thread([this] { handle(); });
|
||||
}
|
||||
~ProcessingQueue()
|
||||
{
|
||||
stop = true;
|
||||
cv.notify_all();
|
||||
handler.join();
|
||||
}
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
@ -1,107 +0,0 @@
|
||||
#include "global.h"
|
||||
#include "../core/core.h"
|
||||
#include <chrono>
|
||||
|
||||
void Soundux::Hooks::internal::onKeyEvent(int key, bool down)
|
||||
{
|
||||
if (translateHotkeys)
|
||||
{
|
||||
if (down && !capturedKeyStates[key].first)
|
||||
{
|
||||
std::vector<std::tuple<int, bool, std::chrono::system_clock::time_point>> pressedStates;
|
||||
for (auto keyState : capturedKeyStates)
|
||||
{
|
||||
if (keyState.second.first)
|
||||
{
|
||||
pressedStates.push_back(
|
||||
std::make_tuple(keyState.first, keyState.second.first, keyState.second.second));
|
||||
}
|
||||
}
|
||||
|
||||
pressedStates.push_back(std::make_tuple(key, down, std::chrono::system_clock::now()));
|
||||
|
||||
std::sort(pressedStates.begin(), pressedStates.end(),
|
||||
[](const auto &left, const auto &right) { return std::get<2>(left) < std::get<2>(right); });
|
||||
|
||||
capturedKeyList.clear();
|
||||
for (auto &item : pressedStates)
|
||||
{
|
||||
capturedKeyList.push_back(std::get<0>(item));
|
||||
}
|
||||
|
||||
QList<QString> stateStr;
|
||||
for (auto &item : pressedStates)
|
||||
{
|
||||
stateStr.push_back(QString::fromStdString(getKeyName(std::get<0>(item))));
|
||||
}
|
||||
emit gCore.keyPress(stateStr);
|
||||
}
|
||||
|
||||
capturedKeyStates[key] = std::make_pair(down, std::chrono::system_clock::now());
|
||||
return;
|
||||
}
|
||||
|
||||
pressedKeys[key] = down;
|
||||
|
||||
if (down && !translateHotkeys)
|
||||
{
|
||||
bool shouldStop = !Config::gConfig.stopHotKey.empty();
|
||||
for (const auto &hotKey : Config::gConfig.stopHotKey)
|
||||
{
|
||||
if (!pressedKeys[hotKey])
|
||||
{
|
||||
shouldStop = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldStop)
|
||||
{
|
||||
gCore.stopPlayback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Config::gConfig.tabHotkeysOnly)
|
||||
{
|
||||
for (auto &sound : Config::gConfig.tabs[Config::gConfig.currentTab].sounds)
|
||||
{
|
||||
bool allPressed = !sound.hotKeys.empty();
|
||||
for (const auto &hotKey : sound.hotKeys)
|
||||
{
|
||||
if (!pressedKeys[hotKey])
|
||||
{
|
||||
allPressed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allPressed)
|
||||
{
|
||||
gCore.playSound(sound);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto &tab : Config::gConfig.tabs)
|
||||
{
|
||||
for (const auto &sound : tab.sounds)
|
||||
{
|
||||
bool allPressed = !sound.hotKeys.empty();
|
||||
for (const auto &hotKey : sound.hotKeys)
|
||||
{
|
||||
if (!pressedKeys[hotKey])
|
||||
{
|
||||
allPressed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allPressed)
|
||||
{
|
||||
gCore.playSound(sound);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
#include "../config/config.h"
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Hooks
|
||||
{
|
||||
// Defined by linux/windows.h
|
||||
std::string getKeyName(int key);
|
||||
|
||||
namespace internal
|
||||
{
|
||||
inline std::map<int, bool> pressedKeys;
|
||||
|
||||
inline bool translateHotkeys = false;
|
||||
inline std::vector<int> capturedKeyList;
|
||||
inline std::map<int, std::pair<bool, std::chrono::system_clock::time_point>> capturedKeyStates;
|
||||
|
||||
void onKeyEvent(int key, bool down);
|
||||
} // namespace internal
|
||||
} // namespace Hooks
|
||||
} // namespace Soundux
|
@ -1,142 +0,0 @@
|
||||
#include <X11/Xlib.h>
|
||||
#ifdef __linux__
|
||||
#pragma once
|
||||
#include "global.h"
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/extensions/XI2.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Hooks
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
inline std::thread keyListener;
|
||||
inline std::atomic<bool> killThread = false;
|
||||
inline Display *display = []() -> Display * {
|
||||
const char *displayenv = std::getenv("DISPLAY");
|
||||
Display *x11display = XOpenDisplay(displayenv);
|
||||
|
||||
if (x11display == nullptr)
|
||||
{
|
||||
std::cerr << "Failed to get X11-Display with value provided by environment variable(" << displayenv
|
||||
<< "), falling back "
|
||||
"to `:0`"
|
||||
<< std::endl;
|
||||
x11display = XOpenDisplay(":0");
|
||||
}
|
||||
|
||||
if (x11display == nullptr)
|
||||
{
|
||||
std::cerr << "Failed to open X11 Display" << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
return x11display;
|
||||
}();
|
||||
|
||||
inline void hook()
|
||||
{
|
||||
int xiOpCode = 0;
|
||||
int queryEvent = 0;
|
||||
int queryError = 0;
|
||||
|
||||
if (XQueryExtension(display, "XInputExtension", &xiOpCode, &queryEvent, &queryError) == 0)
|
||||
{
|
||||
std::cerr << "XInput extension is not aviable" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Custom context
|
||||
{
|
||||
int major = 2;
|
||||
int minor = 0;
|
||||
int queryResult = XIQueryVersion(display, &major, &minor);
|
||||
|
||||
if (queryResult == BadRequest)
|
||||
{
|
||||
std::cerr << "XI 2.0 support is required - Current Version: " << major << "." << minor
|
||||
<< std::endl;
|
||||
return;
|
||||
}
|
||||
if (queryResult != Success)
|
||||
{
|
||||
std::cerr << "Unknown error" << std::endl;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Window root = DefaultRootWindow(display);
|
||||
XIEventMask mask;
|
||||
mask.deviceid = XIAllMasterDevices;
|
||||
mask.mask_len = XIMaskLen(XI_LASTEVENT);
|
||||
mask.mask = static_cast<unsigned char *>(calloc(mask.mask_len, sizeof(char)));
|
||||
|
||||
XISetMask(mask.mask, XI_RawKeyPress);
|
||||
XISetMask(mask.mask, XI_RawKeyRelease);
|
||||
XISelectEvents(display, root, &mask, 1);
|
||||
XSync(display, 0);
|
||||
free(mask.mask);
|
||||
|
||||
while (!killThread.load())
|
||||
{
|
||||
while (!killThread.load())
|
||||
{
|
||||
if (XPending(display) != 0)
|
||||
{
|
||||
XEvent event;
|
||||
XNextEvent(display, &event);
|
||||
auto *cookie = reinterpret_cast<XGenericEventCookie *>(&event.xcookie);
|
||||
|
||||
if ((XGetEventData(display, cookie) != 0) && cookie->type == GenericEvent &&
|
||||
cookie->extension == xiOpCode &&
|
||||
(cookie->evtype == XI_RawKeyRelease || cookie->evtype == XI_RawKeyPress))
|
||||
{
|
||||
auto *ev = reinterpret_cast<XIRawEvent *>(cookie->data);
|
||||
auto key = ev->detail;
|
||||
|
||||
internal::onKeyEvent(key, cookie->evtype == XI_RawKeyPress);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace internal
|
||||
|
||||
inline void setup()
|
||||
{
|
||||
internal::keyListener = std::thread(internal::hook);
|
||||
}
|
||||
inline void stop()
|
||||
{
|
||||
internal::killThread.store(true);
|
||||
internal::keyListener.join();
|
||||
}
|
||||
|
||||
inline std::string getKeyName(const int key)
|
||||
{
|
||||
KeySym s = XkbKeycodeToKeysym(internal::display, key, 0, 0);
|
||||
if (NoSymbol == s)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
char *str = XKeysymToString(s);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
} // namespace Hooks
|
||||
} // namespace Soundux
|
||||
#endif
|
@ -1,93 +0,0 @@
|
||||
#ifdef _WIN32
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <iostream>
|
||||
#include "global.h"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Hooks
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
inline HHOOK oKeyBoardProc;
|
||||
inline std::thread keyListener;
|
||||
inline std::atomic<bool> killThread = false;
|
||||
|
||||
inline LRESULT CALLBACK LLkeyBoardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode == HC_ACTION)
|
||||
{
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
|
||||
{
|
||||
PKBDLLHOOKSTRUCT info = reinterpret_cast<PKBDLLHOOKSTRUCT>(lParam);
|
||||
internal::onKeyEvent(info->vkCode, true);
|
||||
}
|
||||
else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
|
||||
{
|
||||
PKBDLLHOOKSTRUCT info = reinterpret_cast<PKBDLLHOOKSTRUCT>(lParam);
|
||||
internal::onKeyEvent(info->vkCode, false);
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(oKeyBoardProc, nCode, wParam, lParam);
|
||||
}
|
||||
} // namespace internal
|
||||
inline void setup()
|
||||
{
|
||||
internal::keyListener = std::thread([&] {
|
||||
internal::oKeyBoardProc = SetWindowsHookEx(WH_KEYBOARD_LL, internal::LLkeyBoardProc, NULL, NULL);
|
||||
|
||||
MSG message;
|
||||
while (!internal::killThread.load())
|
||||
{
|
||||
PeekMessage(&message, 0, 0, 0, PM_REMOVE);
|
||||
TranslateMessage(&message);
|
||||
DispatchMessage(&message);
|
||||
}
|
||||
|
||||
UnhookWindowsHookEx(internal::oKeyBoardProc);
|
||||
});
|
||||
}
|
||||
inline void stop()
|
||||
{
|
||||
internal::killThread.store(true);
|
||||
internal::keyListener.join();
|
||||
}
|
||||
|
||||
inline std::string getKeyName(const int key)
|
||||
{
|
||||
UINT scanCode = MapVirtualKey(key, MAPVK_VK_TO_VSC);
|
||||
|
||||
CHAR name[128];
|
||||
int result = 0;
|
||||
switch (key)
|
||||
{
|
||||
case VK_LEFT:
|
||||
case VK_UP:
|
||||
case VK_RIGHT:
|
||||
case VK_DOWN:
|
||||
case VK_RCONTROL:
|
||||
case VK_RMENU:
|
||||
case VK_LWIN:
|
||||
case VK_RWIN:
|
||||
case VK_APPS:
|
||||
case VK_PRIOR:
|
||||
case VK_NEXT:
|
||||
case VK_END:
|
||||
case VK_HOME:
|
||||
case VK_INSERT:
|
||||
case VK_DELETE:
|
||||
case VK_DIVIDE:
|
||||
case VK_NUMLOCK:
|
||||
scanCode |= KF_EXTENDED;
|
||||
default:
|
||||
result = GetKeyNameTextA(scanCode << 16, name, 128);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
} // namespace Hooks
|
||||
} // namespace Soundux
|
||||
#endif
|
@ -1,55 +0,0 @@
|
||||
; Script generated by the Inno Script Studio Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Soundux"
|
||||
#define MyAppVersion "0.1.6"
|
||||
#define MyAppPublisher "Soundux"
|
||||
#define MyAppURL "https://soundux.github.io"
|
||||
#define MyAppExeName "soundux.exe"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
; Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{CEF755B2-DDD3-4E35-8DF7-3BDBF86893DE}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={pf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
AllowNoIcons=yes
|
||||
LicenseFile=$PATH\LICENSE
|
||||
OutputBaseFilename=setup
|
||||
SetupIconFile=$PATH\icon.ico
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1
|
||||
|
||||
[Files]
|
||||
Source: "$PATH\build\Release\soundux.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "$PATH\build\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "$PATH\VBCABLE_Driver_Pack43\*"; DestDir: "{app}\VBCable"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\VBCable\VBCABLE_Setup_x64.exe"; WorkingDir: "{app}\VBCable"; Flags: 64bit; Description: "Install VB Cable"; Components: VBCable
|
||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait postinstall skipifsilent; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"
|
||||
|
||||
[Components]
|
||||
Name: "VBCable"; Description: "Install VBCable (recommended)"; Types: full
|
190
src/main.cpp
Executable file → Normal file
@ -1,175 +1,59 @@
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <qqml.h>
|
||||
#include <qurl.h>
|
||||
#include <string>
|
||||
#include "core/global/globals.hpp"
|
||||
#include "helper/exceptions/crashhandler.hpp"
|
||||
#include "ui/impl/webview/webview.hpp"
|
||||
#include <InstanceGuard.hpp>
|
||||
#include <fancy.hpp>
|
||||
|
||||
#include "core/core.h"
|
||||
#include "bindings/bindings.h"
|
||||
#include "config/config.h"
|
||||
#include "playback/global.h"
|
||||
#include "runguard/runguard.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "hotkeys/windows.h"
|
||||
#include "playback/windows.h"
|
||||
#if defined(_WIN32)
|
||||
int __stdcall WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
#else
|
||||
#ifdef __linux__
|
||||
#include "hotkeys/linux.h"
|
||||
#include "playback/linux.h"
|
||||
#include <csignal>
|
||||
#include <execinfo.h>
|
||||
#else
|
||||
// #include "hotkeys/mac.h"
|
||||
int main()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
void sigHandler(int signal)
|
||||
{
|
||||
if (signal == 8)
|
||||
{
|
||||
std::cerr << "This crash is probably related to a bad pulseaudio config" << std::endl;
|
||||
}
|
||||
|
||||
std::cerr << "Received Signal: " << signal << std::endl;
|
||||
std::cerr << "Backtrace available" << std::endl;
|
||||
|
||||
void *elements[20];
|
||||
auto size = backtrace(elements, 20);
|
||||
auto *stack = backtrace_symbols(elements, size);
|
||||
|
||||
for (int i = 0; size > i; i++)
|
||||
{
|
||||
std::cerr << stack[i] << std::endl;
|
||||
}
|
||||
free(stack);
|
||||
|
||||
exit(1);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
DWORD lMode;
|
||||
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
GetConsoleMode(hStdout, &lMode);
|
||||
SetConsoleMode(hStdout, lMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
|
||||
#endif
|
||||
|
||||
void exceptionHandler()
|
||||
{
|
||||
std::cerr << "An exception caused the program to crash" << std::endl;
|
||||
auto exception = std::current_exception();
|
||||
CrashHandler::init();
|
||||
|
||||
try
|
||||
InstanceGuard::InstanceGuard guard("soundux-guard");
|
||||
if (guard.IsAnotherInstanceRunning())
|
||||
{
|
||||
std::rethrow_exception(exception);
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
std::cerr << "Exception: " << e.what() << std::endl;
|
||||
std::cerr << "Exception Type: " << typeid(e).name() << std::endl;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Unknown exception" << std::endl;
|
||||
Fancy::fancy.logTime().failure() << "Another Instance is already running!" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
std::cerr << "Backtrace available" << std::endl;
|
||||
void *elements[20];
|
||||
auto size = backtrace(elements, 20);
|
||||
auto *stack = backtrace_symbols(elements, size);
|
||||
|
||||
for (int i = 0; size > i; i++)
|
||||
#if defined(__linux__)
|
||||
if (!Soundux::Globals::gPulse.isSwitchOnConnectLoaded())
|
||||
{
|
||||
std::cerr << stack[i] << std::endl;
|
||||
Soundux::Globals::gPulse.setup();
|
||||
}
|
||||
free(stack);
|
||||
Soundux::Globals::gAudio.setup();
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
Soundux::RunGuard guard("soundux");
|
||||
if (!guard.tryToRun())
|
||||
Soundux::Globals::gConfig.load();
|
||||
#if defined(__linux__)
|
||||
if (Soundux::Globals::gConfig.settings.useAsDefaultDevice)
|
||||
{
|
||||
std::cerr << "Soundux is already running!";
|
||||
return 0;
|
||||
}
|
||||
|
||||
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
#ifdef _WIN32
|
||||
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
|
||||
std::vector<char *> args(argv, argv + argc);
|
||||
|
||||
// Thanks to this article https://kb.froglogic.com/squish/qt/howto/automating-native-file-dialogs/ !
|
||||
args.push_back(const_cast<char *>("-platformtheme"));
|
||||
args.push_back(const_cast<char *>("none"));
|
||||
|
||||
argc = args.size();
|
||||
argv = args.data();
|
||||
|
||||
QApplication app(argc, (char **)args.data());
|
||||
#else
|
||||
QApplication app(argc, argv);
|
||||
#endif
|
||||
QQmlApplicationEngine engine;
|
||||
QQuickStyle::setStyle("Material");
|
||||
|
||||
app.setOrganizationDomain("https://github.com/D3SOX/Soundux");
|
||||
app.setOrganizationName("Soundux");
|
||||
|
||||
Soundux::Config::loadConfig();
|
||||
Soundux::Playback::usedDevices = Soundux::Config::gConfig.volumes;
|
||||
|
||||
gCore.setEngine(&engine);
|
||||
engine.rootContext()->setContextProperty("core", &gCore);
|
||||
|
||||
// register meta types
|
||||
qRegisterMetaType<QTab>();
|
||||
qRegisterMetaType<std::vector<QTab>>();
|
||||
|
||||
qRegisterMetaType<QSound>();
|
||||
qRegisterMetaType<std::vector<QSound>>();
|
||||
|
||||
#ifdef __linux__
|
||||
qRegisterMetaType<QPulseAudioRecordingStream>();
|
||||
qRegisterMetaType<std::vector<QPulseAudioRecordingStream>>();
|
||||
#endif
|
||||
|
||||
Soundux::Hooks::setup();
|
||||
std::set_terminate(exceptionHandler);
|
||||
|
||||
#ifdef __linux__
|
||||
signal(SIGSEGV, sigHandler);
|
||||
signal(SIGABRT, sigHandler);
|
||||
signal(SIGFPE, sigHandler);
|
||||
|
||||
Soundux::Playback::createSink();
|
||||
|
||||
for (const auto &device : Soundux::Playback::getPlaybackDevices())
|
||||
{
|
||||
if (device.name == Soundux::Playback::internal::sinkName)
|
||||
{
|
||||
gCore.setLinuxSink(device);
|
||||
break;
|
||||
}
|
||||
Soundux::Globals::gPulse.setDefaultSourceToSoundboardSink();
|
||||
}
|
||||
#endif
|
||||
Soundux::Globals::gData = Soundux::Globals::gConfig.data;
|
||||
Soundux::Globals::gSettings = Soundux::Globals::gConfig.settings;
|
||||
|
||||
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
|
||||
static_cast<void>(app.exec());
|
||||
Soundux::Globals::gGui = std::make_unique<Soundux::Objects::WebView>();
|
||||
Soundux::Globals::gGui->setup();
|
||||
Soundux::Globals::gGui->mainLoop();
|
||||
|
||||
Soundux::Config::gConfig.volumes = Soundux::Playback::usedDevices;
|
||||
Soundux::Config::saveConfig();
|
||||
|
||||
Soundux::Hooks::stop();
|
||||
|
||||
Soundux::Playback::stopAllAudio();
|
||||
#ifdef __linux__
|
||||
Soundux::Playback::deleteSink();
|
||||
Soundux::Globals::gAudio.destory();
|
||||
#if defined(__linux__)
|
||||
Soundux::Globals::gPulse.destroy();
|
||||
#endif
|
||||
Soundux::Playback::destroy();
|
||||
Soundux::Globals::gConfig.data = Soundux::Globals::gData;
|
||||
Soundux::Globals::gConfig.settings = Soundux::Globals::gSettings;
|
||||
Soundux::Globals::gConfig.save();
|
||||
|
||||
return 0;
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
#pragma once
|
||||
#include "../config/config.h"
|
||||
#include <atomic>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <map>
|
||||
#include <miniaudio.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Playback
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
struct PlayingSound
|
||||
{
|
||||
std::uint64_t id;
|
||||
ma_device *device;
|
||||
ma_decoder *decoder;
|
||||
Config::Sound sound;
|
||||
};
|
||||
struct DefaultDevice
|
||||
{
|
||||
std::string name;
|
||||
};
|
||||
|
||||
inline std::mutex playingSoundsMutext;
|
||||
inline std::vector<PlayingSound> playingSounds;
|
||||
|
||||
void data_callback(ma_device *device, void *output, const void *input, std::uint32_t frameCount);
|
||||
} // namespace internal
|
||||
|
||||
inline std::map<std::string, float> usedDevices;
|
||||
|
||||
Playback::internal::DefaultDevice getDefaultCaptureDevice();
|
||||
Playback::internal::DefaultDevice getDefaultPlaybackDevice();
|
||||
|
||||
inline auto defaultCapture = getDefaultCaptureDevice();
|
||||
inline auto defaultPlayback = getDefaultPlaybackDevice();
|
||||
|
||||
std::vector<ma_device_info> getCaptureDevices();
|
||||
std::vector<ma_device_info> getPlaybackDevices();
|
||||
|
||||
std::vector<internal::PlayingSound> getPlayingSounds();
|
||||
|
||||
void setVolume(const std::string &deviceName, float volume);
|
||||
|
||||
std::uint64_t playAudio(const Config::Sound &sound);
|
||||
std::uint64_t playAudio(const Config::Sound &sound, const ma_device_info &deviceInfo);
|
||||
|
||||
void stop(const std::uint64_t &deviceId);
|
||||
inline std::function<void(const internal::PlayingSound &)> stopCallback = [](const auto &device) {};
|
||||
|
||||
void pause(const std::uint64_t &deviceId);
|
||||
|
||||
void resume(const std::uint64_t &deviceId);
|
||||
|
||||
void stopAllAudio();
|
||||
|
||||
namespace internal
|
||||
{
|
||||
inline std::atomic<bool> killGarbageCollector = false;
|
||||
|
||||
inline std::map<ma_device *, bool> deviceClearQueue;
|
||||
|
||||
inline auto garbageCollector = [] {
|
||||
std::thread collector([] {
|
||||
while (!killGarbageCollector.load())
|
||||
{
|
||||
for (auto sound = deviceClearQueue.begin(); deviceClearQueue.end() != sound; ++sound)
|
||||
{
|
||||
if (sound->second)
|
||||
{
|
||||
playingSoundsMutext.lock();
|
||||
for (unsigned int i = 0; playingSounds.size() > i; i++)
|
||||
{
|
||||
auto &device = playingSounds.at(i);
|
||||
if (device.device != sound->first)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
stop(device.id);
|
||||
}
|
||||
playingSoundsMutext.unlock();
|
||||
deviceClearQueue.erase(sound);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
});
|
||||
return collector;
|
||||
}();
|
||||
} // namespace internal
|
||||
inline void destroy()
|
||||
{
|
||||
internal::killGarbageCollector.store(true);
|
||||
internal::garbageCollector.join();
|
||||
}
|
||||
} // namespace Playback
|
||||
} // namespace Soundux
|
@ -1,305 +0,0 @@
|
||||
#include "global.h"
|
||||
#include <cstdint>
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include <miniaudio.h>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
Playback::internal::DefaultDevice Playback::getDefaultPlaybackDevice()
|
||||
{
|
||||
ma_device device;
|
||||
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback);
|
||||
ma_device_init(nullptr, &deviceConfig, &device);
|
||||
|
||||
Playback::internal::DefaultDevice playbackInfo;
|
||||
playbackInfo.name = device.playback.name;
|
||||
|
||||
ma_device_uninit(&device);
|
||||
|
||||
return playbackInfo;
|
||||
}
|
||||
Playback::internal::DefaultDevice Playback::getDefaultCaptureDevice()
|
||||
{
|
||||
ma_device device;
|
||||
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_capture);
|
||||
ma_device_init(nullptr, &deviceConfig, &device);
|
||||
|
||||
Playback::internal::DefaultDevice captureInfo;
|
||||
captureInfo.name = device.capture.name;
|
||||
|
||||
ma_device_uninit(&device);
|
||||
|
||||
return captureInfo;
|
||||
}
|
||||
std::vector<ma_device_info> Playback::getPlaybackDevices()
|
||||
{
|
||||
ma_context context;
|
||||
|
||||
if (ma_context_init(nullptr, 0, nullptr, &context) != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to initialize context" << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
ma_device_info *pPlayBackDeviceInfos{};
|
||||
ma_uint32 deviceCount{};
|
||||
|
||||
ma_result result = ma_context_get_devices(&context, &pPlayBackDeviceInfos, &deviceCount, nullptr, nullptr);
|
||||
if (result != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to get playback devices!" << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ma_device_info> playBackDevices;
|
||||
for (unsigned int i = 0; deviceCount > i; i++)
|
||||
{
|
||||
playBackDevices.push_back(pPlayBackDeviceInfos[i]);
|
||||
}
|
||||
|
||||
ma_context_uninit(&context);
|
||||
|
||||
return playBackDevices;
|
||||
}
|
||||
std::vector<ma_device_info> Playback::getCaptureDevices()
|
||||
{
|
||||
ma_context context;
|
||||
|
||||
if (ma_context_init(nullptr, 0, nullptr, &context) != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to initialize context" << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
ma_device_info *pCaptureDeviceInfos{};
|
||||
ma_uint32 deviceCount{};
|
||||
|
||||
ma_result result = ma_context_get_devices(&context, &pCaptureDeviceInfos, &deviceCount, nullptr, nullptr);
|
||||
if (result != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to get playback devices!" << std::endl;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ma_device_info> captureDevices;
|
||||
for (unsigned int i = 0; deviceCount > i; i++)
|
||||
{
|
||||
captureDevices.push_back(pCaptureDeviceInfos[i]);
|
||||
}
|
||||
|
||||
ma_context_uninit(&context);
|
||||
|
||||
return captureDevices;
|
||||
}
|
||||
void Playback::setVolume(const std::string &deviceName, float volume)
|
||||
{
|
||||
usedDevices[deviceName] = volume;
|
||||
}
|
||||
std::uint64_t Playback::playAudio(const Config::Sound &sound)
|
||||
{
|
||||
static std::uint64_t counter = 0;
|
||||
|
||||
//? Theoretically we could remove this, but this will result in the defaultPlayBackVolume being 0. This will
|
||||
//? only change when the user manually changes this value in the ui where the default value will not match.
|
||||
if (usedDevices.find(defaultPlayback.name) == usedDevices.end())
|
||||
{
|
||||
usedDevices.insert(std::make_pair(defaultPlayback.name, 1.F));
|
||||
}
|
||||
|
||||
auto *decoder = new ma_decoder;
|
||||
ma_result result = ma_decoder_init_file(sound.path.c_str(), nullptr, decoder);
|
||||
|
||||
if (result != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to init decoder" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto *device = new ma_device;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = decoder->outputFormat;
|
||||
config.playback.channels = decoder->outputChannels;
|
||||
config.sampleRate = decoder->outputSampleRate;
|
||||
config.dataCallback = internal::data_callback;
|
||||
config.pUserData = decoder;
|
||||
|
||||
if (ma_device_init(nullptr, &config, device) != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to open playback device" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
if (ma_device_start(device) != MA_SUCCESS)
|
||||
{
|
||||
ma_device_uninit(device);
|
||||
ma_decoder_uninit(decoder);
|
||||
std::cerr << "Failed to start playback device" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
internal::playingSoundsMutext.lock();
|
||||
internal::playingSounds.push_back({
|
||||
++counter,
|
||||
device,
|
||||
decoder,
|
||||
sound,
|
||||
});
|
||||
internal::playingSoundsMutext.unlock();
|
||||
|
||||
return counter;
|
||||
}
|
||||
std::uint64_t Playback::playAudio(const Config::Sound &sound, const ma_device_info &deviceInfo)
|
||||
{
|
||||
static std::uint64_t counter = 0;
|
||||
|
||||
if (usedDevices.find(deviceInfo.name) == usedDevices.end())
|
||||
{
|
||||
usedDevices.insert(std::make_pair(deviceInfo.name, 1.F));
|
||||
}
|
||||
|
||||
auto *decoder = new ma_decoder;
|
||||
ma_result result = ma_decoder_init_file(sound.path.c_str(), nullptr, decoder);
|
||||
|
||||
if (result != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to init decoder" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto *device = new ma_device;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = decoder->outputFormat;
|
||||
config.playback.channels = decoder->outputChannels;
|
||||
config.sampleRate = decoder->outputSampleRate;
|
||||
config.dataCallback = internal::data_callback;
|
||||
config.playback.pDeviceID = &deviceInfo.id;
|
||||
config.pUserData = decoder;
|
||||
|
||||
if (ma_device_init(nullptr, &config, device) != MA_SUCCESS)
|
||||
{
|
||||
std::cerr << "Failed to open playback device" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
if (ma_device_start(device) != MA_SUCCESS)
|
||||
{
|
||||
ma_device_uninit(device);
|
||||
ma_decoder_uninit(decoder);
|
||||
std::cerr << "Failed to start playback device" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
internal::playingSoundsMutext.lock();
|
||||
internal::playingSounds.push_back({++counter, device, decoder, sound});
|
||||
internal::playingSoundsMutext.unlock();
|
||||
|
||||
return counter;
|
||||
}
|
||||
void Playback::stop(const std::uint64_t &deviceId)
|
||||
{
|
||||
// No need to lock the mutex here, it only gets called when the mutex is locked!
|
||||
for (unsigned int i = 0; internal::playingSounds.size() > i; i++)
|
||||
{
|
||||
auto &device = internal::playingSounds.at(i);
|
||||
if (device.id == deviceId)
|
||||
{
|
||||
if (device.device && device.decoder)
|
||||
{
|
||||
stopCallback(device);
|
||||
|
||||
ma_device_uninit(device.device);
|
||||
ma_decoder_uninit(device.decoder);
|
||||
|
||||
delete device.device;
|
||||
delete device.decoder;
|
||||
|
||||
device.device = nullptr;
|
||||
device.decoder = nullptr;
|
||||
}
|
||||
|
||||
internal::playingSounds.erase(internal::playingSounds.begin() + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Playback::pause(const std::uint64_t &deviceId)
|
||||
{
|
||||
internal::playingSoundsMutext.lock();
|
||||
for (unsigned int i = 0; internal::playingSounds.size() > i; i++)
|
||||
{
|
||||
auto &device = internal::playingSounds.at(i);
|
||||
|
||||
if (device.id == deviceId)
|
||||
{
|
||||
ma_device_stop(device.device);
|
||||
break;
|
||||
}
|
||||
}
|
||||
internal::playingSoundsMutext.unlock();
|
||||
}
|
||||
void Playback::resume(const std::uint64_t &deviceId)
|
||||
{
|
||||
internal::playingSoundsMutext.lock();
|
||||
for (unsigned int i = 0; internal::playingSounds.size() > i; i++)
|
||||
{
|
||||
auto &device = internal::playingSounds.at(i);
|
||||
|
||||
if (device.id == deviceId)
|
||||
{
|
||||
ma_device_start(device.device);
|
||||
break;
|
||||
}
|
||||
}
|
||||
internal::playingSoundsMutext.unlock();
|
||||
}
|
||||
void Playback::stopAllAudio()
|
||||
{
|
||||
internal::playingSoundsMutext.lock();
|
||||
for (unsigned int i = 0; internal::playingSounds.size() > i; i++)
|
||||
{
|
||||
auto &device = internal::playingSounds.at(i);
|
||||
if (device.device && device.decoder)
|
||||
{
|
||||
stopCallback(device);
|
||||
|
||||
ma_device_uninit(device.device);
|
||||
ma_decoder_uninit(device.decoder);
|
||||
|
||||
delete device.device;
|
||||
delete device.decoder;
|
||||
|
||||
device.device = nullptr;
|
||||
device.decoder = nullptr;
|
||||
}
|
||||
}
|
||||
internal::playingSounds.clear();
|
||||
internal::playingSoundsMutext.unlock();
|
||||
}
|
||||
void Playback::internal::data_callback(ma_device *device, void *output, [[maybe_unused]] const void *input,
|
||||
std::uint32_t frameCount)
|
||||
{
|
||||
auto *decoder = reinterpret_cast<ma_decoder *>(device->pUserData);
|
||||
if (decoder == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (usedDevices.find(device->playback.name) != usedDevices.end())
|
||||
{
|
||||
device->masterVolumeFactor = usedDevices[device->playback.name];
|
||||
}
|
||||
|
||||
auto readFrames = ma_decoder_read_pcm_frames(decoder, output, frameCount);
|
||||
|
||||
if (readFrames <= 0)
|
||||
{
|
||||
internal::deviceClearQueue[device] = true;
|
||||
}
|
||||
}
|
||||
std::vector<Playback::internal::PlayingSound> Playback::getPlayingSounds()
|
||||
{
|
||||
Playback::internal::playingSoundsMutext.lock();
|
||||
auto rtn = Playback::internal::playingSounds;
|
||||
Playback::internal::playingSoundsMutext.unlock();
|
||||
|
||||
return rtn;
|
||||
}
|
||||
} // namespace Soundux
|
@ -1,223 +0,0 @@
|
||||
#pragma once
|
||||
#ifdef __linux__
|
||||
#include <memory>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "global.h"
|
||||
#include "../config/config.h"
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Playback
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
inline std::string sinkMonitorId;
|
||||
inline std::string sinkId;
|
||||
inline std::string loopBackId;
|
||||
inline const std::string sinkName = "soundboard_sink";
|
||||
|
||||
inline std::string getOutput(const std::string &command)
|
||||
{
|
||||
std::array<char, 128> buffer;
|
||||
std::string result;
|
||||
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(command.c_str(), "r"), pclose);
|
||||
|
||||
if (!pipe)
|
||||
{
|
||||
throw std::runtime_error("popen failed");
|
||||
}
|
||||
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
|
||||
{
|
||||
result += buffer.data();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::vector<std::string> splitByNewLine(const std::string &str)
|
||||
{
|
||||
auto result = std::vector<std::string>{};
|
||||
auto ss = std::stringstream{str};
|
||||
|
||||
for (std::string line; std::getline(ss, line, '\n');)
|
||||
{
|
||||
result.push_back(line);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
inline std::string getDefaultCaptureDevice()
|
||||
{
|
||||
// get default input device
|
||||
std::string defaultInput;
|
||||
auto result = getOutput("LC_ALL=C pactl info");
|
||||
|
||||
static const std::regex reg(R"rgx(Default Source: (.+))rgx");
|
||||
std::smatch sm;
|
||||
|
||||
if (std::regex_search(result, sm, reg))
|
||||
defaultInput = sm[1].str();
|
||||
else
|
||||
std::cerr << "Failed to get default capture device" << std::endl;
|
||||
|
||||
return defaultInput;
|
||||
}
|
||||
|
||||
struct PulseAudioRecordingStream
|
||||
{
|
||||
int index = -1;
|
||||
std::string driver;
|
||||
std::string source;
|
||||
std::string resampleMethod;
|
||||
std::string processBinary;
|
||||
|
||||
operator bool() const
|
||||
{
|
||||
return index >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
inline bool isValidDevice(const PulseAudioRecordingStream &stream)
|
||||
{
|
||||
return stream.driver == "protocol-native.c" && stream.resampleMethod != "peaks";
|
||||
}
|
||||
} // namespace internal
|
||||
|
||||
inline void createSink()
|
||||
{
|
||||
using internal::getOutput;
|
||||
|
||||
auto sinkId = getOutput(("pactl load-module module-null-sink sink_name=" + internal::sinkName +
|
||||
" rate=44100 sink_properties=device.description=" + internal::sinkName));
|
||||
internal::sinkId = sinkId;
|
||||
|
||||
auto defaultInput = internal::getDefaultCaptureDevice();
|
||||
// Create loopback for input
|
||||
if (!defaultInput.empty())
|
||||
{
|
||||
auto createLoopBack = "pactl load-module module-loopback rate=44100 source=\"" + defaultInput +
|
||||
"\" sink=\"" + internal::sinkName + "\"";
|
||||
|
||||
auto loopBackId = getOutput(createLoopBack);
|
||||
internal::loopBackId = loopBackId;
|
||||
}
|
||||
|
||||
auto sources = getOutput("LC_ALL=C pactl list sources");
|
||||
auto sourcesSplit = internal::splitByNewLine(sources);
|
||||
|
||||
struct
|
||||
{
|
||||
std::string id;
|
||||
std::string name;
|
||||
} device{};
|
||||
|
||||
static const std::regex sourceRegex(R"rgx((.*#(\d+))$|(Name: (.+)))rgx");
|
||||
std::smatch match;
|
||||
|
||||
for (const std::string &line : sourcesSplit)
|
||||
{
|
||||
if (std::regex_search(line, match, sourceRegex))
|
||||
{
|
||||
if (match[2].matched)
|
||||
{
|
||||
device.id = match[2];
|
||||
}
|
||||
else if (match[4].matched)
|
||||
{
|
||||
device.name = match[4];
|
||||
}
|
||||
|
||||
if (device.name == internal::sinkName + ".monitor")
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (device.name != internal::sinkName + ".monitor")
|
||||
{
|
||||
std::cerr << "Failed to find soundboard sink in PulseAudio sources!" << std::endl;
|
||||
}
|
||||
|
||||
internal::sinkMonitorId = device.id;
|
||||
};
|
||||
inline void deleteSink()
|
||||
{
|
||||
system(("pactl unload-module " + internal::loopBackId).c_str());
|
||||
system(("pactl unload-module " + internal::sinkId).c_str());
|
||||
};
|
||||
inline auto getSources()
|
||||
{
|
||||
using internal::getOutput;
|
||||
using internal::PulseAudioRecordingStream;
|
||||
using internal::splitByNewLine;
|
||||
|
||||
auto input = getOutput("LC_ALL=C pactl list source-outputs");
|
||||
|
||||
auto splitted = splitByNewLine(input);
|
||||
std::vector<PulseAudioRecordingStream> streams;
|
||||
|
||||
static const auto regex = std::regex(
|
||||
R"rgx((.*#(\d+))|(Driver: (.+))|(Source: (\d+))|(.*process.*binary.* = "(.+)")|(Resample method: (.+)))rgx");
|
||||
|
||||
PulseAudioRecordingStream stream;
|
||||
for (auto &line : splitted)
|
||||
{
|
||||
std::smatch match;
|
||||
if (std::regex_search(line, match, regex))
|
||||
{
|
||||
if (match[2].matched)
|
||||
{
|
||||
if (stream && isValidDevice(stream))
|
||||
{
|
||||
streams.push_back(stream);
|
||||
}
|
||||
|
||||
stream = {};
|
||||
stream.index = std::stoi(match[2]);
|
||||
}
|
||||
else if (stream)
|
||||
{
|
||||
if (match[4].matched)
|
||||
{
|
||||
stream.driver = match[4];
|
||||
}
|
||||
else if (match[6].matched)
|
||||
{
|
||||
stream.source = match[6];
|
||||
}
|
||||
else if (match[8].matched)
|
||||
{
|
||||
stream.processBinary = match[8];
|
||||
}
|
||||
else if (match[10].matched)
|
||||
{
|
||||
stream.resampleMethod = match[10];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stream && isValidDevice(stream))
|
||||
{
|
||||
streams.push_back(stream);
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
inline std::optional<internal::PulseAudioRecordingStream> getCurrentOutputApplication()
|
||||
{
|
||||
auto sources = Soundux::Playback::getSources();
|
||||
if (sources.size() > Soundux::Config::gConfig.currentOutputApplication)
|
||||
{
|
||||
return sources[Soundux::Config::gConfig.currentOutputApplication];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace Playback
|
||||
} // namespace Soundux
|
||||
#endif
|
@ -1,12 +0,0 @@
|
||||
#ifdef _WIN32
|
||||
#pragma once
|
||||
#include "global.h"
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Playback
|
||||
{
|
||||
//! Windows doesn't require additional implementations.
|
||||
} // namespace Playback
|
||||
} // namespace Soundux
|
||||
#endif
|
@ -1,37 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import "./" as Components
|
||||
|
||||
Button {
|
||||
id: control
|
||||
property string iconName
|
||||
|
||||
Material.elevation: 5
|
||||
|
||||
font.capitalization: Font.MixedCase
|
||||
font.pointSize: 9.5
|
||||
implicitHeight: 42
|
||||
contentItem: Item {
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Components.MaterialDesignIcon {
|
||||
name: iconName
|
||||
size: control.font.pixelSize * 1.4
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
// width: control.width
|
||||
elide: Text.ElideRight
|
||||
text: control.text
|
||||
font: control.font
|
||||
color: !control.enabled ? control.Material.hintTextColor : control.flat
|
||||
&& control.highlighted ? control.Material.accentColor : control.highlighted ? control.Material.primaryHighlightedTextColor : control.Material.foreground
|
||||
visible: !(control.text === "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import "../resources/MaterialDesign.js" as MD
|
||||
|
||||
Item {
|
||||
property real size: 24
|
||||
property string name
|
||||
property color color: Material.foreground
|
||||
|
||||
width: size
|
||||
height: size
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
|
||||
color: parent.color
|
||||
|
||||
font.family: materialFont.name
|
||||
font.pixelSize: parent.height
|
||||
|
||||
text: MD.icons["mdi_" + parent.name]
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: materialFont
|
||||
source: "../../lib/materialdesignicons/fonts/materialdesignicons-webfont.ttf"
|
||||
}
|
||||
}
|
828
src/qml/main.qml
@ -1,828 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Dialogs 1.3 as Dialogs
|
||||
import "./components/" as Components
|
||||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
visible: true
|
||||
width: 995
|
||||
height: 550
|
||||
title: "Soundux"
|
||||
minimumWidth: 850
|
||||
minimumHeight: 500
|
||||
|
||||
property var isWindows: core.isWindows()
|
||||
property var darkMode: core.getDarkMode()
|
||||
property var cBg: darkMode ? Material.color(Material.Grey, Material.Shade900) : Material.color(Material.Grey, Material.Shade400)
|
||||
property var cBgDarker: darkMode ? "#313131" : Material.color(Material.Grey, Material.Shade500)
|
||||
property var cBgDarkest: darkMode ? Material.color(Material.Grey, Material.Shade800) : Material.color(Material.Grey, Material.Shade600)
|
||||
|
||||
Material.theme: darkMode ? Material.Dark : Material.Light
|
||||
Material.background: cBg
|
||||
|
||||
Material.accent: Material.Green
|
||||
property var currentTab: undefined
|
||||
property var lastModifedLocal: true
|
||||
|
||||
onWidthChanged:
|
||||
{
|
||||
core.onSizeChanged(width, height)
|
||||
}
|
||||
onHeightChanged:
|
||||
{
|
||||
core.onSizeChanged(width, height)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+F"
|
||||
onActivated: {
|
||||
searchBtn.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
core.loadSettings();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: core
|
||||
function onInvalidApplication() {
|
||||
invalidAppDialog.visible = true
|
||||
refreshOutputBtn.clicked();
|
||||
}
|
||||
function onSetLocalVolume(volume)
|
||||
{
|
||||
localVolume.value = volume * 100
|
||||
}
|
||||
function onSetRemoteVolume(volume)
|
||||
{
|
||||
remoteVolume.value = volume * 100
|
||||
}
|
||||
function onSetOutputApplication(index)
|
||||
{
|
||||
outputApplicationBox.currentIndex = index
|
||||
}
|
||||
function onSetSize(width, height)
|
||||
{
|
||||
window.width = width
|
||||
window.height = height
|
||||
}
|
||||
function onUpdateCurrentTab()
|
||||
{
|
||||
soundsListStack.updateItems();
|
||||
}
|
||||
}
|
||||
|
||||
Dialog
|
||||
{
|
||||
id: invalidAppDialog
|
||||
visible: false
|
||||
modal: true
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Label
|
||||
{
|
||||
text: "Invalid output application!"
|
||||
}
|
||||
|
||||
standardButtons: Dialog.Ok
|
||||
}
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
source: "resources/icon.jpg"
|
||||
width: 64
|
||||
fillMode: Image.PreserveAspectFit
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 10
|
||||
anchors.leftMargin: 5
|
||||
}
|
||||
|
||||
Label {
|
||||
id: programName
|
||||
font.pointSize: 22
|
||||
anchors.left: icon.right
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
anchors.leftMargin: 5
|
||||
text: "Soundux"
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
id: stopButton
|
||||
text: "Stop"
|
||||
iconName: "stop"
|
||||
enabled: false
|
||||
anchors.top: icon.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 5
|
||||
anchors.leftMargin: 5
|
||||
height: 60
|
||||
width: 130
|
||||
|
||||
Connections {
|
||||
target: core
|
||||
function onPlaybackChanged(state) {
|
||||
stopButton.enabled = state
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
core.stopPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
id: volumePane
|
||||
anchors.left: stopButton.right
|
||||
anchors.right: syncVolumeStates.left
|
||||
anchors.top: stopButton.top
|
||||
anchors.bottom: stopButton.bottom
|
||||
anchors.leftMargin: 35
|
||||
|
||||
GridLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
columns: 2
|
||||
rows: 2
|
||||
rowSpacing: -20
|
||||
Label {
|
||||
text: "Local volume"
|
||||
Layout.column: 0
|
||||
Layout.row: 0
|
||||
}
|
||||
Slider {
|
||||
id: localVolume
|
||||
|
||||
value: 100
|
||||
stepSize: 1
|
||||
from: 0
|
||||
to: 100
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.column: 1
|
||||
Layout.row: 0
|
||||
|
||||
ToolTip {
|
||||
parent: localVolume.handle
|
||||
visible: localVolume.pressed
|
||||
text: value.toFixed(0)
|
||||
|
||||
readonly property int value: localVolume.valueAt(localVolume.position)
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
core.changeLocalVolume(localVolume.value)
|
||||
lastModifedLocal = true
|
||||
if (syncVolumeStates.checked)
|
||||
{
|
||||
remoteVolume.value = localVolume.value
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: "Remote volume"
|
||||
Layout.column: 0
|
||||
Layout.row: 1
|
||||
}
|
||||
Slider {
|
||||
id: remoteVolume
|
||||
|
||||
value: 100
|
||||
stepSize: 1
|
||||
from: 0
|
||||
to: 100
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.column: 1
|
||||
Layout.row: 1
|
||||
|
||||
ToolTip {
|
||||
parent: remoteVolume.handle
|
||||
visible: remoteVolume.pressed
|
||||
text: value.toFixed(0)
|
||||
|
||||
readonly property int value: remoteVolume.valueAt(remoteVolume.position)
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
core.changeRemoteVolume(remoteVolume.value)
|
||||
lastModifedLocal = false
|
||||
if (syncVolumeStates.checked)
|
||||
{
|
||||
localVolume.value = remoteVolume.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox
|
||||
{
|
||||
text: "Sync"
|
||||
id: syncVolumeStates
|
||||
anchors.right: controlPane.left
|
||||
anchors.verticalCenter: volumePane.verticalCenter
|
||||
|
||||
onCheckedChanged:
|
||||
{
|
||||
if (lastModifedLocal)
|
||||
{
|
||||
remoteVolume.value = localVolume.value
|
||||
}
|
||||
else
|
||||
{
|
||||
localVolume.value = remoteVolume.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
id: controlPane
|
||||
anchors.right: searchPane.left
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
columns: 1
|
||||
rowSpacing: -5
|
||||
|
||||
ComboBox {
|
||||
id: outputApplicationBox
|
||||
|
||||
model: ListModel {
|
||||
id: outputApplicationModel
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
var outputApplications = isWindows ? core.getPlaybackDevices() : core.getOutputApplications();
|
||||
for (var child in outputApplications) {
|
||||
outputApplicationModel.append({
|
||||
"text": isWindows ? outputApplications[child] : outputApplications[child].getName()
|
||||
})
|
||||
}
|
||||
outputApplicationBox.currentIndex = 0
|
||||
}
|
||||
|
||||
Label {
|
||||
text: isWindows ? "Output Device" : "Output application"
|
||||
anchors.right: parent.left
|
||||
anchors.rightMargin: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
core.currentOutputApplicationChanged(outputApplicationBox.currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
id: refreshOutputBtn
|
||||
Layout.fillWidth: true
|
||||
text: "Refresh"
|
||||
iconName: "reload"
|
||||
|
||||
onClicked: {
|
||||
outputApplicationModel.clear()
|
||||
var outputApplications = isWindows ? core.getPlaybackDevices() : core.getOutputApplications();
|
||||
for (var child in outputApplications) {
|
||||
outputApplicationModel.append({
|
||||
"text": isWindows ? outputApplications[child] : outputApplications[child].getName()
|
||||
})
|
||||
}
|
||||
outputApplicationBox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
id: searchPane
|
||||
visible: false
|
||||
|
||||
width: visible ? implicitWidth : 0
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
property var _sounds;
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
if (visible)
|
||||
{
|
||||
var sounds = core.getAllSounds()
|
||||
_sounds = sounds
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
anchors.fill: parent
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
width: 88
|
||||
placeholderText: "Search sounds..."
|
||||
onTextChanged:
|
||||
{
|
||||
searchList.clear();
|
||||
for (var child in searchPane._sounds) {
|
||||
if (text && searchPane._sounds[child].getName().toLowerCase().includes(text.toLowerCase()))
|
||||
{
|
||||
searchList.append({
|
||||
"name": searchPane._sounds[child].getName(),
|
||||
"path": searchPane._sounds[child].getPath(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView
|
||||
{
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
id: searchResults
|
||||
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
width: 8
|
||||
}
|
||||
|
||||
model: ListModel
|
||||
{
|
||||
id: searchList
|
||||
}
|
||||
spacing: 0
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
|
||||
color: cBgDarker
|
||||
height: 25
|
||||
width: searchResults.width
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: name
|
||||
color: "white"
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: {
|
||||
searchResults.currentIndex = index
|
||||
}
|
||||
onDoubleClicked: {
|
||||
core.playSoundByPath(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: setHotkeyDialog
|
||||
property string soundName;
|
||||
|
||||
title: "Set hotkey for " + soundName
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
standardButtons: Dialog.Ok | Dialog.Reset | Dialog.Cancel
|
||||
|
||||
TextField {
|
||||
id: hotkeyField
|
||||
anchors.fill: parent
|
||||
placeholderText: "Hotkey"
|
||||
readOnly: true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: core
|
||||
function onKeyPress(key) {
|
||||
hotkeyField.text = key.join(" + ")
|
||||
}
|
||||
function onKeyCleared() {
|
||||
hotkeyField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
hotkeyField.text = ""
|
||||
if (visible)
|
||||
{
|
||||
hotkeyField.text = core.getCurrentHotKey(soundsListView.currentItem._path).join(" + ")
|
||||
}
|
||||
}
|
||||
|
||||
onActiveFocusChanged:
|
||||
{
|
||||
if (activeFocus)
|
||||
{
|
||||
hotkeyField.text = ""
|
||||
}
|
||||
core.hotkeyDialogFocusChanged(activeFocus);
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
core.setHotkey(soundsListView.currentItem._path)
|
||||
}
|
||||
|
||||
onReset: {
|
||||
hotkeyField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: settingsDialog
|
||||
title: "Settings"
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
ColumnLayout {
|
||||
CheckBox {
|
||||
checked: true
|
||||
text: "Hotkeys only for current tab"
|
||||
onCheckedChanged:
|
||||
{
|
||||
core.onTabHotkeyOnlyChanged(checked)
|
||||
}
|
||||
Component.onCompleted:
|
||||
{
|
||||
checked = core.getTabHotkeysOnly();
|
||||
}
|
||||
}
|
||||
CheckBox {
|
||||
checked: false
|
||||
text: "Allow sound overlapping"
|
||||
onCheckedChanged:
|
||||
{
|
||||
core.onAllowOverlappingChanged(checked);
|
||||
}
|
||||
Component.onCompleted:
|
||||
{
|
||||
checked = core.getAllowOverlapping();
|
||||
}
|
||||
}
|
||||
CheckBox {
|
||||
checked: true
|
||||
Component.onCompleted:
|
||||
{
|
||||
checked = darkMode;
|
||||
}
|
||||
|
||||
text: "Dark Theme"
|
||||
onCheckedChanged:
|
||||
{
|
||||
darkMode = checked;
|
||||
core.onDarkModeChanged(darkMode);
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Label {
|
||||
text: "Stop hotkey"
|
||||
}
|
||||
TextField {
|
||||
text: "none"
|
||||
id: stopHotkey
|
||||
readOnly: true
|
||||
|
||||
Connections {
|
||||
target: core
|
||||
function onKeyPress(key) {
|
||||
stopHotkey.text = key.join(" + ")
|
||||
}
|
||||
function onKeyCleared() {
|
||||
stopHotkey.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
onActiveFocusChanged:
|
||||
{
|
||||
if(activeFocus)
|
||||
{
|
||||
stopHotkey.text = ""
|
||||
}
|
||||
core.hotkeyDialogFocusChanged(activeFocus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
stopHotkey.text = ""
|
||||
if (visible)
|
||||
{
|
||||
stopHotkey.text = core.getStopHotKey().join(" + ")
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted:
|
||||
{
|
||||
core.setStopHotkey()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: removeTabDialog
|
||||
title: "Remove tab"
|
||||
modal: true
|
||||
anchors.centerIn: parent
|
||||
contentHeight: -20
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
onAccepted: {
|
||||
core.removeTab()
|
||||
}
|
||||
}
|
||||
|
||||
Dialogs.FileDialog {
|
||||
id: addTabDialog
|
||||
title: "Please choose a folder"
|
||||
selectFolder: true
|
||||
folder: shortcuts.home
|
||||
onAccepted: {
|
||||
core.addFolderTab(addTabDialog.fileUrl)
|
||||
}
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: bar
|
||||
contentHeight: 35
|
||||
anchors.top: stopButton.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
anchors.right: controlPane.left
|
||||
|
||||
Repeater {
|
||||
model: ListModel {
|
||||
id: barModel
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: modelData
|
||||
width: implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: core
|
||||
function onFoldersChanged() {
|
||||
barModel.clear()
|
||||
var tabs = core.getTabs()
|
||||
for (var child in tabs) {
|
||||
barModel.append({
|
||||
"text": tabs[child].getTitle()
|
||||
})
|
||||
}
|
||||
bar.currentIndex = -1
|
||||
bar.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
core.currentTabChanged(bar.currentIndex)
|
||||
soundsListStack.updateItems()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
var tabs = core.getTabs()
|
||||
for (var child in tabs) {
|
||||
barModel.append({
|
||||
"text": tabs[child].getTitle()
|
||||
})
|
||||
}
|
||||
bar.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: soundsListStack
|
||||
anchors.top: bar.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: controlPane.left
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
function updateItems() {
|
||||
soundsList.clear()
|
||||
var sounds = core.getSounds()
|
||||
|
||||
for (var child in sounds)
|
||||
{
|
||||
var item =
|
||||
{
|
||||
index: child,
|
||||
name: sounds[child].getName(),
|
||||
path: sounds[child].getPath(),
|
||||
hotKey: sounds[child].getKeyBinds().join("+")
|
||||
}
|
||||
|
||||
soundsList.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateItems()
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: soundsListView
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 5
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
width: 8
|
||||
}
|
||||
|
||||
model: ListModel {
|
||||
id: soundsList
|
||||
}
|
||||
spacing: 0
|
||||
clip: true
|
||||
|
||||
Rectangle
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: cBgDarker
|
||||
z: -1
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
color: soundsListView.currentIndex == index ? cBgDarkest : cBgDarker
|
||||
width: soundsListView.width
|
||||
|
||||
property var _name: name
|
||||
property var _path: path
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: 25
|
||||
|
||||
Text {
|
||||
text: name
|
||||
color: "white"
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
visible: hotKey.length > 0
|
||||
|
||||
radius: 5
|
||||
color: cBg
|
||||
|
||||
height: 20
|
||||
width: hotkeyDisplay.implicitWidth + 10
|
||||
|
||||
anchors.rightMargin: 5
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text
|
||||
{
|
||||
text: hotKey
|
||||
color: "white"
|
||||
id: hotkeyDisplay
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
hoverEnabled: true
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: {
|
||||
soundsListView.currentIndex = index
|
||||
}
|
||||
onDoubleClicked: {
|
||||
core.playSound(index)
|
||||
}
|
||||
ToolTip {
|
||||
x: parent.mouseX + 20
|
||||
y: parent.mouseY + 20
|
||||
|
||||
text: path
|
||||
delay: 1000
|
||||
parent: parent
|
||||
visible: parent.containsMouse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
anchors.top: controlPane.bottom
|
||||
anchors.left: controlPane.left
|
||||
anchors.right: controlPane.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
columns: 1
|
||||
rowSpacing: -5
|
||||
|
||||
Components.IconButton {
|
||||
Layout.fillWidth: true
|
||||
id: searchBtn
|
||||
text: "Search"
|
||||
iconName: "magnify"
|
||||
|
||||
onClicked: {
|
||||
searchPane.visible = !searchPane.visible
|
||||
if (searchPane.visible) {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Add tab"
|
||||
iconName: "folder_plus"
|
||||
|
||||
onClicked: {
|
||||
addTabDialog.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Remove tab"
|
||||
iconName: "tab_minus"
|
||||
|
||||
onClicked: {
|
||||
// TODO: open only when on a tab
|
||||
removeTabDialog.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
id: buttonRefreshFolder
|
||||
Layout.fillWidth: true
|
||||
text: "Refresh"
|
||||
iconName: "refresh"
|
||||
|
||||
onClicked: {
|
||||
core.updateFolderSounds(core.getCurrentTab())
|
||||
soundsListStack.updateItems()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Set hotkey"
|
||||
iconName: "keyboard"
|
||||
|
||||
onClicked: {
|
||||
if (soundsListView.currentIndex >= 0)
|
||||
{
|
||||
setHotkeyDialog.visible = true
|
||||
setHotkeyDialog.soundName = soundsListView.currentItem._name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Play"
|
||||
iconName: "play"
|
||||
|
||||
onClicked: {
|
||||
core.playSound(soundsListView.currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Components.IconButton {
|
||||
Layout.fillWidth: true
|
||||
text: "Settings"
|
||||
iconName: "cog"
|
||||
|
||||
onClicked: {
|
||||
settingsDialog.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>main.qml</file>
|
||||
<file>../../lib/materialdesignicons/fonts/materialdesignicons-webfont.ttf</file>
|
||||
|
||||
<file>components/IconButton.qml</file>
|
||||
<file>components/MaterialDesignIcon.qml</file>
|
||||
|
||||
<file>resources/icon.jpg</file>
|
||||
<file>resources/MaterialDesign.js</file>
|
||||
</qresource>
|
||||
</RCC>
|
Before Width: | Height: | Size: 66 KiB |
@ -1,89 +0,0 @@
|
||||
// By Dmitry Sazonov
|
||||
#pragma once
|
||||
#include <QCryptographicHash>
|
||||
#include <QObject>
|
||||
#include <QSharedMemory>
|
||||
#include <QSystemSemaphore>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
QString generateKeyHash(const QString &key, const QString &salt)
|
||||
{
|
||||
QByteArray data;
|
||||
|
||||
data.append(key.toUtf8());
|
||||
data.append(salt.toUtf8());
|
||||
data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex();
|
||||
|
||||
return data;
|
||||
}
|
||||
} // namespace internal
|
||||
|
||||
class RunGuard // NOLINT
|
||||
{
|
||||
public:
|
||||
RunGuard(const QString &key)
|
||||
: key(key), memLockKey(internal::generateKeyHash(key, "_memLockKey")),
|
||||
sharedmemKey(internal::generateKeyHash(key, "_sharedmemKey")), sharedMem(sharedmemKey),
|
||||
memLock(memLockKey, 1)
|
||||
{
|
||||
memLock.acquire();
|
||||
{
|
||||
QSharedMemory fix(sharedmemKey);
|
||||
fix.attach();
|
||||
}
|
||||
memLock.release();
|
||||
}
|
||||
~RunGuard()
|
||||
{
|
||||
release();
|
||||
}
|
||||
|
||||
bool isAnotherRunning()
|
||||
{
|
||||
if (sharedMem.isAttached())
|
||||
return false;
|
||||
|
||||
memLock.acquire();
|
||||
const bool isRunning = sharedMem.attach();
|
||||
if (isRunning)
|
||||
sharedMem.detach();
|
||||
memLock.release();
|
||||
|
||||
return isRunning;
|
||||
}
|
||||
bool tryToRun()
|
||||
{
|
||||
if (isAnotherRunning())
|
||||
return false;
|
||||
|
||||
memLock.acquire();
|
||||
const bool result = sharedMem.create(sizeof(std::uint64_t));
|
||||
memLock.release();
|
||||
if (!result)
|
||||
{
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
void release()
|
||||
{
|
||||
memLock.acquire();
|
||||
if (sharedMem.isAttached())
|
||||
sharedMem.detach();
|
||||
memLock.release();
|
||||
}
|
||||
|
||||
private:
|
||||
const QString key;
|
||||
const QString memLockKey;
|
||||
const QString sharedmemKey;
|
||||
|
||||
QSharedMemory sharedMem;
|
||||
QSystemSemaphore memLock;
|
||||
};
|
||||
} // namespace Soundux
|
1
src/ui/impl/webview/lib/soundux-ui
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 5e528dfeff2e93953de70cd25912b6a64a5c2a18
|
1
src/ui/impl/webview/lib/webview
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 05f3d12be44a21ccaa4753d2d68adbd3fb726a09
|
318
src/ui/impl/webview/webview.cpp
Normal file
@ -0,0 +1,318 @@
|
||||
#include "webview.hpp"
|
||||
#include "../../../core/global/globals.hpp"
|
||||
#include "../../../helper/json/bindings.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fancy.hpp>
|
||||
#include <filesystem>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shellapi.h>
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
void WebView::setup()
|
||||
{
|
||||
Window::setup();
|
||||
#ifdef _WIN32
|
||||
char rawPath[MAX_PATH];
|
||||
auto executablePath = GetModuleFileNameA(nullptr, rawPath, MAX_PATH);
|
||||
|
||||
auto path = std::filesystem::canonical(rawPath).parent_path() / "dist" / "index.html";
|
||||
#endif
|
||||
#if defined(__linux__)
|
||||
auto path = std::filesystem::canonical("/proc/self/exe").parent_path() / "dist" / "index.html";
|
||||
#endif
|
||||
webview = std::make_unique<wv::WebView>(Globals::gData.width, Globals::gData.height, true, "Soundux",
|
||||
"file://" + path.string());
|
||||
if (!webview->init())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to create UI" << std::endl;
|
||||
}
|
||||
|
||||
webview->addCallback(
|
||||
"getPlayingSounds",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return nlohmann::json(Globals::gAudio.getPlayingSounds()).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"getSettings",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return nlohmann::json(Globals::gSettings).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"getData",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return nlohmann::json(Globals::gData).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"addTab",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
auto tab = addTab();
|
||||
if (tab)
|
||||
{
|
||||
return nlohmann::json(*tab).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"playSound",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto sound = playSound(std::stoi(param[0]));
|
||||
if (sound)
|
||||
{
|
||||
return nlohmann::json(*sound).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback("stopSound", [this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
if (stopSound(std::stoi(param[0])))
|
||||
{
|
||||
return "true";
|
||||
}
|
||||
return "false";
|
||||
});
|
||||
webview->addCallback("stopSounds",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
stopSounds();
|
||||
return "";
|
||||
});
|
||||
webview->addCallback(
|
||||
"pauseSound",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto sound = pauseSound(std::stoi(param[0]));
|
||||
if (sound)
|
||||
{
|
||||
return nlohmann::json(*sound).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"resumeSound",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto sound = resumeSound(std::stoi(param[0]));
|
||||
if (sound)
|
||||
{
|
||||
return nlohmann::json(*sound).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"seekSound",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto sound = seekSound(std::stoi(param[0]), std::stoi(param[1]));
|
||||
if (sound)
|
||||
{
|
||||
return nlohmann::json(*sound).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback("changeSettings", [this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
nlohmann::json j = nlohmann::json::parse(param[0]);
|
||||
changeSettings(j.get<Settings>());
|
||||
return "";
|
||||
});
|
||||
webview->addCallback("requestHotkey", [this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
if (param[0] == "false")
|
||||
{
|
||||
Globals::gHotKeys.shouldNotify(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Globals::gHotKeys.shouldNotify(true);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
webview->addCallback(
|
||||
"setHotkey",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto sound = setHotkey(std::stoi(param[0]), nlohmann::json::parse("[" + param[1] + "]"));
|
||||
if (sound)
|
||||
{
|
||||
return nlohmann::json(*sound).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback("getHotkeySequence", [this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
nlohmann::json j = nlohmann::json::parse("[" + param[0] + "]");
|
||||
return getHotkeySequence(j.get<std::vector<int>>());
|
||||
});
|
||||
webview->addCallback(
|
||||
"removeTab",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
return nlohmann::json(removeTab(std::stoi(param[0]))).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"refreshTab",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto tab = refreshTab(std::stoi(param[0]));
|
||||
if (tab)
|
||||
{
|
||||
return nlohmann::json(*tab).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"repeatSound",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto playingSound = repeatSound(std::stoi(param[0]), param[1] == "true");
|
||||
if (playingSound)
|
||||
{
|
||||
return nlohmann::json(*playingSound).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"moveTabs",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
std::vector<int> newTabOrder = nlohmann::json::parse("[" + param[0] + "]");
|
||||
return nlohmann::json(changeTabOrder(newTabOrder)).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"isLinux",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
#if defined(__linux__)
|
||||
return "true";
|
||||
#else
|
||||
return "false";
|
||||
#endif
|
||||
},
|
||||
true);
|
||||
|
||||
#if !defined(__linux__)
|
||||
webview->addCallback(
|
||||
"getOutput",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return nlohmann::json(getOutput()).dump();
|
||||
},
|
||||
true);
|
||||
#if defined(_WIN32)
|
||||
webview->addCallback("openUrl", [this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
ShellExecuteA(0, 0, param[0].c_str(), 0, 0, SW_SHOW);
|
||||
return "";
|
||||
});
|
||||
#else
|
||||
// TODO(curve): Mac
|
||||
#endif
|
||||
#else
|
||||
webview->addCallback("openUrl", [this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
system(("xdg-open \"" + param[0] + "\"").c_str());
|
||||
return "";
|
||||
});
|
||||
webview->addCallback(
|
||||
"getOutput",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return nlohmann::json(getOutput()).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"getPlayback",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return nlohmann::json(getPlayback()).dump();
|
||||
},
|
||||
true);
|
||||
webview->addCallback(
|
||||
"startPassthrough",
|
||||
[this]([[maybe_unused]] auto &wv, const auto ¶m) -> std::string {
|
||||
auto playback = startPassthrough(param[0]);
|
||||
if (playback)
|
||||
{
|
||||
return nlohmann::json(*playback).dump();
|
||||
}
|
||||
return "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback("stopPassthrough",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
stopPassthrough();
|
||||
return "";
|
||||
});
|
||||
webview->addCallback(
|
||||
"isSwitchOnConnectLoaded",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
return Globals::gPulse.isSwitchOnConnectLoaded() ? "true" : "false";
|
||||
},
|
||||
true);
|
||||
webview->addCallback("unloadSwitchOnConnect",
|
||||
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto ¶m) -> std::string {
|
||||
if (Globals::gPulse.isSwitchOnConnectLoaded())
|
||||
{
|
||||
Globals::gPulse.unloadSwitchOnConnect();
|
||||
Globals::gPulse.setup();
|
||||
}
|
||||
return "";
|
||||
});
|
||||
#endif
|
||||
|
||||
webview->setResizeCallback([this](int width, int height) {
|
||||
Globals::gData.width = width;
|
||||
Globals::gData.height = height;
|
||||
});
|
||||
}
|
||||
void WebView::mainLoop()
|
||||
{
|
||||
while (webview->run())
|
||||
{
|
||||
progressEvents();
|
||||
#if defined(_WIN32)
|
||||
Sleep(20);
|
||||
#endif
|
||||
}
|
||||
Fancy::fancy.logTime() << "UI exited" << std::endl;
|
||||
}
|
||||
void WebView::onHotKeyReceived(const std::vector<int> &keys)
|
||||
{
|
||||
std::string hotkeySequence;
|
||||
for (const auto &key : keys)
|
||||
{
|
||||
hotkeySequence += Globals::gHotKeys.getKeyName(key) + " + ";
|
||||
}
|
||||
auto js = "window.hotkeyReceived(`" + hotkeySequence.substr(0, hotkeySequence.length() - 3) +
|
||||
"`, JSON.parse(`" + nlohmann::json(keys).dump() + "`));";
|
||||
|
||||
onEvent([js, this]() { webview->eval(js); });
|
||||
}
|
||||
void WebView::onSoundFinished(const PlayingSound &sound)
|
||||
{
|
||||
Window::onSoundFinished(sound);
|
||||
auto soundObj = nlohmann::json(sound).dump();
|
||||
auto js = "window.finishSound(JSON.parse(`" + soundObj + "`));";
|
||||
|
||||
onEvent([js, this]() { webview->eval(js); });
|
||||
}
|
||||
void WebView::onSoundPlayed(const PlayingSound &sound)
|
||||
{
|
||||
auto soundObj = nlohmann::json(sound).dump();
|
||||
auto js = "window.onSoundPlayed(JSON.parse(`" + soundObj + "`));";
|
||||
|
||||
onEvent([js, this]() { webview->eval(js); });
|
||||
}
|
||||
void WebView::onSoundProgressed(const PlayingSound &sound)
|
||||
{
|
||||
auto soundObj = nlohmann::json(sound).dump();
|
||||
auto js = "window.updateSound(JSON.parse(`" + soundObj + "`));";
|
||||
|
||||
onEvent([js, this]() { webview->eval(js); });
|
||||
}
|
||||
void WebView::onError(const ErrorCode &error)
|
||||
{
|
||||
auto js = "window.onError(" + std::to_string(static_cast<std::uint8_t>(error)) + ");";
|
||||
onEvent([js, this]() { webview->eval(js); });
|
||||
}
|
||||
} // namespace Soundux::Objects
|
25
src/ui/impl/webview/webview.hpp
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "../../ui.hpp"
|
||||
#include "lib/webview/webview.hpp"
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
class WebView : public Window
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<wv::WebView> webview;
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void mainLoop() override;
|
||||
void onSoundFinished(const PlayingSound &sound) override;
|
||||
void onHotKeyReceived(const std::vector<int> &keys) override;
|
||||
|
||||
void onError(const ErrorCode &error) override;
|
||||
void onSoundPlayed(const PlayingSound &sound) override;
|
||||
void onSoundProgressed(const PlayingSound &sound) override;
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|
487
src/ui/ui.cpp
Normal file
@ -0,0 +1,487 @@
|
||||
#include "ui.hpp"
|
||||
#include "../core/global/globals.hpp"
|
||||
#include <cstdint>
|
||||
#include <fancy.hpp>
|
||||
#include <filesystem>
|
||||
#include <nfd.hpp>
|
||||
#include <optional>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
#endif
|
||||
|
||||
namespace Soundux::Objects
|
||||
{
|
||||
void Window::setup()
|
||||
{
|
||||
NFD::Init();
|
||||
Globals::gHotKeys.init();
|
||||
for (auto &tab : Globals::gData.getTabs())
|
||||
{
|
||||
tab.sounds = refreshTabSounds(tab);
|
||||
Globals::gData.setTab(tab.id, tab);
|
||||
}
|
||||
}
|
||||
Window::~Window()
|
||||
{
|
||||
NFD::Quit();
|
||||
Globals::gHotKeys.stop();
|
||||
}
|
||||
std::vector<Sound> Window::refreshTabSounds(const Tab &tab) const
|
||||
{
|
||||
std::vector<Sound> rtn;
|
||||
for (const auto &entry : std::filesystem::directory_iterator(tab.path))
|
||||
{
|
||||
std::filesystem::path file = entry;
|
||||
if (entry.is_symlink())
|
||||
{
|
||||
file = std::filesystem::read_symlink(entry);
|
||||
if (file.has_relative_path())
|
||||
{
|
||||
file = std::filesystem::canonical(tab.path / file);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.extension() != ".mp3" && file.extension() != ".wav" && file.extension() != ".flac")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Sound sound;
|
||||
|
||||
std::error_code ec;
|
||||
auto writeTime = std::filesystem::last_write_time(file, ec);
|
||||
if (!ec)
|
||||
{
|
||||
sound.modifiedDate = writeTime.time_since_epoch().count();
|
||||
}
|
||||
else
|
||||
{
|
||||
Fancy::fancy.logTime().warning() << "Failed to read lastWriteTime of " << file << std::endl;
|
||||
}
|
||||
|
||||
sound.path = file.u8string();
|
||||
#if defined(_WIN32)
|
||||
std::transform(sound.path.begin(), sound.path.end(), sound.path.begin(),
|
||||
[](char c) { return c == '\\' ? '/' : c; });
|
||||
#endif
|
||||
sound.name = file.stem().u8string();
|
||||
sound.id = ++Globals::gData.soundIdCounter;
|
||||
|
||||
if (auto oldSound = std::find_if(tab.sounds.begin(), tab.sounds.end(),
|
||||
[&sound](const auto &item) { return item.path == sound.path; });
|
||||
oldSound != tab.sounds.end())
|
||||
{
|
||||
sound.hotkeys = oldSound->hotkeys;
|
||||
}
|
||||
|
||||
rtn.emplace_back(sound);
|
||||
}
|
||||
|
||||
std::sort(rtn.begin(), rtn.end(),
|
||||
[](const auto &first, const auto &second) { return first.modifiedDate > second.modifiedDate; });
|
||||
|
||||
return rtn;
|
||||
}
|
||||
std::optional<Tab> Window::addTab() // NOLINT
|
||||
{
|
||||
nfdnchar_t *outpath = {};
|
||||
auto result = NFD::PickFolder(outpath, nullptr);
|
||||
if (result == NFD_OKAY)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
std::wstring wpath(outpath);
|
||||
std::string path = std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(wpath.c_str()); // NOLINT
|
||||
std::transform(path.begin(), path.end(), path.begin(), [](char c) { return c == '\\' ? '/' : c; });
|
||||
#else
|
||||
std::string path(outpath);
|
||||
#endif
|
||||
NFD_FreePathN(outpath);
|
||||
|
||||
if (std::filesystem::exists(path))
|
||||
{
|
||||
Tab tab;
|
||||
tab.path = path;
|
||||
tab.sounds = refreshTabSounds(tab);
|
||||
tab.name = std::filesystem::path(path).filename().u8string();
|
||||
|
||||
tab = Globals::gData.addTab(std::move(tab));
|
||||
|
||||
return tab;
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Selected Folder does not exist!" << std::endl;
|
||||
onError(ErrorCode::FolderDoesNotExist);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
#if defined(__linux__)
|
||||
std::optional<PlayingSound> Window::playSound(const std::uint32_t &id)
|
||||
{
|
||||
auto sound = Globals::gData.getSound(id);
|
||||
if (sound)
|
||||
{
|
||||
if (Globals::gPulse.moveApplicationToSinkMonitor(Globals::gSettings.output))
|
||||
{
|
||||
if (!Globals::gSettings.allowOverlapping)
|
||||
{
|
||||
Globals::gAudio.stopAll();
|
||||
}
|
||||
auto playingSound = Globals::gAudio.play(*sound);
|
||||
auto remotePlayingSound = Globals::gAudio.play(*sound, Globals::gAudio.sinkAudioDevice, true);
|
||||
|
||||
if (playingSound && remotePlayingSound)
|
||||
{
|
||||
std::unique_lock lock(groupedSoundsMutex);
|
||||
groupedSounds.insert({playingSound->id, remotePlayingSound->id});
|
||||
return *playingSound;
|
||||
}
|
||||
|
||||
if (playingSound)
|
||||
stopSound(playingSound->id);
|
||||
if (remotePlayingSound)
|
||||
stopSound(remotePlayingSound->id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move Application '" << Globals::gSettings.output
|
||||
<< "' to soundux sink for sound " << id << std::endl;
|
||||
onError(ErrorCode::FailedToMoveToSink);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to play sound " << id << std::endl;
|
||||
onError(ErrorCode::FailedToPlay);
|
||||
return std::nullopt;
|
||||
}
|
||||
#else
|
||||
std::optional<PlayingSound> Window::playSound(const std::uint32_t &id)
|
||||
{
|
||||
auto sound = Globals::gData.getSound(id);
|
||||
auto device = Globals::gAudio.getAudioDevice(Globals::gSettings.output);
|
||||
|
||||
if (sound && device)
|
||||
{
|
||||
if (!Globals::gSettings.allowOverlapping)
|
||||
{
|
||||
Globals::gAudio.stopAll();
|
||||
}
|
||||
auto playingSound = Globals::gAudio.play(*sound);
|
||||
auto remotePlayingSound = Globals::gAudio.play(*sound, *device, true);
|
||||
|
||||
if (playingSound && remotePlayingSound)
|
||||
{
|
||||
std::unique_lock lock(groupedSoundsMutex);
|
||||
groupedSounds.insert({playingSound->id, remotePlayingSound->id});
|
||||
return *playingSound;
|
||||
}
|
||||
|
||||
if (playingSound)
|
||||
stopSound(playingSound->id);
|
||||
if (remotePlayingSound)
|
||||
stopSound(remotePlayingSound->id);
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to play sound " << id << std::endl;
|
||||
onError(ErrorCode::FailedToPlay);
|
||||
return std::nullopt;
|
||||
}
|
||||
#endif
|
||||
std::optional<PlayingSound> Window::pauseSound(const std::uint32_t &id)
|
||||
{
|
||||
std::shared_lock lock(groupedSoundsMutex);
|
||||
if (groupedSounds.find(id) == groupedSounds.end())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to find remoteSound of sound " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto playingSound = Globals::gAudio.pause(id);
|
||||
auto remotePlayingSound = Globals::gAudio.pause(groupedSounds.at(id));
|
||||
|
||||
if (playingSound && remotePlayingSound)
|
||||
{
|
||||
return *playingSound;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to pause sound " << id << std::endl;
|
||||
onError(ErrorCode::FailedToPause);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<PlayingSound> Window::resumeSound(const std::uint32_t &id)
|
||||
{
|
||||
std::shared_lock lock(groupedSoundsMutex);
|
||||
if (groupedSounds.find(id) == groupedSounds.end())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to find remoteSound of sound " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto playingSound = Globals::gAudio.resume(id);
|
||||
auto remotePlayingSound = Globals::gAudio.resume(groupedSounds.at(id));
|
||||
|
||||
if (playingSound && remotePlayingSound)
|
||||
{
|
||||
return *playingSound;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to resume sound " << id << std::endl;
|
||||
onError(ErrorCode::FailedToResume);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<PlayingSound> Window::seekSound(const std::uint32_t &id, std::uint64_t seekTo)
|
||||
{
|
||||
std::shared_lock lock(groupedSoundsMutex);
|
||||
if (groupedSounds.find(id) == groupedSounds.end())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to find remoteSound of sound " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto playingSound = Globals::gAudio.seek(id, seekTo);
|
||||
auto remotePlayingSound = Globals::gAudio.seek(groupedSounds.at(id), seekTo);
|
||||
if (playingSound && remotePlayingSound)
|
||||
{
|
||||
return *playingSound;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to seek sound " << id << " to " << seekTo << std::endl;
|
||||
onError(ErrorCode::FailedToSeek);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<PlayingSound> Window::repeatSound(const std::uint32_t &id, bool shouldRepeat)
|
||||
{
|
||||
std::shared_lock lock(groupedSoundsMutex);
|
||||
if (groupedSounds.find(id) == groupedSounds.end())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to find remoteSound of sound " << id << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto playingSound = Globals::gAudio.repeat(id, shouldRepeat);
|
||||
auto remotePlayingSound = Globals::gAudio.repeat(groupedSounds.at(id), shouldRepeat);
|
||||
if (playingSound && remotePlayingSound)
|
||||
{
|
||||
return *playingSound;
|
||||
}
|
||||
|
||||
Fancy::fancy.logTime().failure() << "Failed to set repeatstate of sound " << id << " to " << shouldRepeat
|
||||
<< std::endl;
|
||||
onError(ErrorCode::FailedToRepeat);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<Tab> Window::removeTab(const std::uint32_t &id)
|
||||
{
|
||||
Globals::gData.removeTabById(id);
|
||||
return Globals::gData.getTabs();
|
||||
}
|
||||
bool Window::stopSound(const std::uint32_t &id)
|
||||
{
|
||||
std::shared_lock lock(groupedSoundsMutex);
|
||||
if (groupedSounds.find(id) == groupedSounds.end())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to find remoteSound of sound " << id << std::endl;
|
||||
return false;
|
||||
}
|
||||
auto remoteId = groupedSounds.at(id);
|
||||
lock.unlock();
|
||||
|
||||
auto status = Globals::gAudio.stop(id);
|
||||
auto remoteStatus = Globals::gAudio.stop(remoteId);
|
||||
|
||||
#if defined(__linux__)
|
||||
if (Globals::gAudio.getPlayingSounds().empty())
|
||||
{
|
||||
if (!Globals::gPulse.moveBackCurrentApplication())
|
||||
{
|
||||
Fancy::fancy.logTime().failure()
|
||||
<< "Failed to move back current application, sound: " << id << std::endl;
|
||||
onError(ErrorCode::FailedToMoveBack);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return status && remoteStatus;
|
||||
}
|
||||
void Window::stopSounds()
|
||||
{
|
||||
Globals::gQueue.push_unique(0, []() { Globals::gAudio.stopAll(); });
|
||||
#if defined(__linux__)
|
||||
if (!Globals::gPulse.moveBackCurrentApplication())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move back current application" << std::endl;
|
||||
onError(ErrorCode::FailedToMoveBack);
|
||||
}
|
||||
if (!Globals::gPulse.moveBackApplicationFromPassthrough())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move back current passthrough application" << std::endl;
|
||||
onError(ErrorCode::FailedToMoveBackPassthrough);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void Window::changeSettings(const Settings &settings)
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (!settings.useAsDefaultDevice && Globals::gSettings.useAsDefaultDevice)
|
||||
{
|
||||
if (!Globals::gPulse.revertDefaultSourceToOriginal())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move back default source" << std::endl;
|
||||
onError(ErrorCode::FailedToRevertDefaultSource);
|
||||
}
|
||||
}
|
||||
else if (settings.useAsDefaultDevice && !Globals::gSettings.useAsDefaultDevice)
|
||||
{
|
||||
if (!Globals::gPulse.setDefaultSourceToSoundboardSink())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to set default source" << std::endl;
|
||||
onError(ErrorCode::FailedToSetDefaultSource);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Globals::gSettings = settings;
|
||||
}
|
||||
void Window::onHotKeyReceived([[maybe_unused]] const std::vector<int> &keys)
|
||||
{
|
||||
Globals::gHotKeys.shouldNotify(false);
|
||||
}
|
||||
void Window::onEvent(const std::function<void()> &function)
|
||||
{
|
||||
std::unique_lock lock(eventMutex);
|
||||
eventQueue.emplace(function);
|
||||
lock.unlock();
|
||||
|
||||
shouldCheck = true;
|
||||
}
|
||||
void Window::progressEvents()
|
||||
{
|
||||
if (shouldCheck)
|
||||
{
|
||||
std::shared_lock lock(eventMutex);
|
||||
if (!eventQueue.empty())
|
||||
{
|
||||
lock.unlock();
|
||||
{
|
||||
std::unique_lock uLock(eventMutex);
|
||||
while (!eventQueue.empty())
|
||||
{
|
||||
auto front = std::move(eventQueue.front());
|
||||
eventQueue.pop();
|
||||
|
||||
uLock.unlock();
|
||||
front();
|
||||
uLock.lock();
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
}
|
||||
std::optional<Tab> Window::refreshTab(const std::uint32_t &id)
|
||||
{
|
||||
auto tab = Globals::gData.getTab(id);
|
||||
if (tab)
|
||||
{
|
||||
tab->sounds = refreshTabSounds(*tab);
|
||||
auto newTab = Globals::gData.setTab(id, *tab);
|
||||
if (newTab)
|
||||
{
|
||||
return newTab;
|
||||
}
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to refresh tab " << id << " tab does not exist" << std::endl;
|
||||
onError(ErrorCode::TabDoesNotExist);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<Sound> Window::setHotkey(const std::uint32_t &id, const std::vector<int> &hotkeys)
|
||||
{
|
||||
auto sound = Globals::gData.getSound(id);
|
||||
if (sound)
|
||||
{
|
||||
sound->get().hotkeys = hotkeys;
|
||||
return sound->get();
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to set hotkey for sound " << id << ", sound does not exist"
|
||||
<< std::endl;
|
||||
onError(ErrorCode::FailedToSetHotkey);
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string Window::getHotkeySequence(const std::vector<int> &hotkeys)
|
||||
{
|
||||
return Globals::gHotKeys.getKeySequence(hotkeys);
|
||||
}
|
||||
std::vector<Tab> Window::changeTabOrder(const std::vector<int> &newOrder)
|
||||
{
|
||||
std::vector<Tab> newTabs;
|
||||
newTabs.reserve(newOrder.size());
|
||||
|
||||
for (auto tabId : newOrder)
|
||||
{
|
||||
newTabs.emplace_back(*Globals::gData.getTab(tabId));
|
||||
}
|
||||
Globals::gData.setTabs(newTabs);
|
||||
return Globals::gData.getTabs();
|
||||
}
|
||||
#if defined(__linux__)
|
||||
std::vector<PulseRecordingStream> Window::getOutput()
|
||||
{
|
||||
Globals::gPulse.refreshRecordingStreams();
|
||||
return Globals::gPulse.getRecordingStreams();
|
||||
}
|
||||
std::vector<PulsePlaybackStream> Window::getPlayback()
|
||||
{
|
||||
Globals::gPulse.refreshPlaybackStreams();
|
||||
return Globals::gPulse.getPlaybackStreams();
|
||||
}
|
||||
std::optional<PulsePlaybackStream> Window::startPassthrough(const std::string &name)
|
||||
{
|
||||
if (Globals::gPulse.moveApplicationToSinkMonitor(Globals::gSettings.output))
|
||||
{
|
||||
return Globals::gPulse.moveApplicationToApplicationPassthrough(name);
|
||||
}
|
||||
Fancy::fancy.logTime().failure() << "Failed to start passthrough for application: " << name << std::endl;
|
||||
onError(ErrorCode::FailedToStartPassthrough);
|
||||
return std::nullopt;
|
||||
}
|
||||
void Window::stopPassthrough()
|
||||
{
|
||||
if (Globals::gAudio.getPlayingSounds().empty())
|
||||
{
|
||||
if (!Globals::gPulse.moveBackCurrentApplication())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move back current application" << std::endl;
|
||||
onError(ErrorCode::FailedToMoveBack);
|
||||
}
|
||||
}
|
||||
if (!Globals::gPulse.moveBackApplicationFromPassthrough())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move back current passthrough application" << std::endl;
|
||||
onError(ErrorCode::FailedToMoveBackPassthrough);
|
||||
}
|
||||
}
|
||||
#else
|
||||
std::vector<AudioDevice> Window::getOutput()
|
||||
{
|
||||
Globals::gAudio.refreshAudioDevices();
|
||||
return Globals::gAudio.getAudioDevices();
|
||||
}
|
||||
#endif
|
||||
void Window::onSoundFinished(const PlayingSound &sound)
|
||||
{
|
||||
std::unique_lock lock(groupedSoundsMutex);
|
||||
if (groupedSounds.find(sound.id) != groupedSounds.end())
|
||||
{
|
||||
groupedSounds.erase(sound.id);
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
if (Globals::gAudio.getPlayingSounds().size() == 1 && !Globals::gPulse.currentlyPassingthrough())
|
||||
{
|
||||
if (!Globals::gPulse.moveBackCurrentApplication())
|
||||
{
|
||||
Fancy::fancy.logTime().failure() << "Failed to move back current application" << std::endl;
|
||||
onError(ErrorCode::FailedToMoveBack);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace Soundux::Objects
|
71
src/ui/ui.hpp
Normal file
@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include "../core/global/objects.hpp"
|
||||
#include "../helper/audio/audio.hpp"
|
||||
#if defined(__linux__)
|
||||
#include "../helper/audio/linux/pulse.hpp"
|
||||
#endif
|
||||
#include <cstdint>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
|
||||
namespace Soundux
|
||||
{
|
||||
namespace Objects
|
||||
{
|
||||
class Window
|
||||
{
|
||||
friend class Hotkeys;
|
||||
|
||||
protected:
|
||||
std::shared_mutex groupedSoundsMutex;
|
||||
std::map<std::uint32_t, std::uint32_t> groupedSounds;
|
||||
|
||||
std::shared_mutex eventMutex;
|
||||
std::atomic<bool> shouldCheck = false;
|
||||
std::queue<std::function<void()>> eventQueue;
|
||||
virtual void progressEvents();
|
||||
|
||||
virtual void stopSounds();
|
||||
virtual bool stopSound(const std::uint32_t &);
|
||||
virtual std::vector<Tab> removeTab(const std::uint32_t &);
|
||||
virtual std::optional<Tab> refreshTab(const std::uint32_t &);
|
||||
virtual std::optional<PlayingSound> playSound(const std::uint32_t &);
|
||||
virtual std::optional<PlayingSound> pauseSound(const std::uint32_t &);
|
||||
virtual std::optional<PlayingSound> resumeSound(const std::uint32_t &);
|
||||
virtual std::optional<PlayingSound> repeatSound(const std::uint32_t &, bool);
|
||||
virtual std::optional<PlayingSound> seekSound(const std::uint32_t &, std::uint64_t);
|
||||
virtual std::optional<Sound> setHotkey(const std::uint32_t &, const std::vector<int> &);
|
||||
|
||||
virtual std::string getHotkeySequence(const std::vector<int> &);
|
||||
|
||||
virtual void changeSettings(const Settings &);
|
||||
|
||||
virtual std::optional<Tab> addTab();
|
||||
virtual std::vector<Sound> refreshTabSounds(const Tab &) const;
|
||||
virtual std::vector<Tab> changeTabOrder(const std::vector<int> &);
|
||||
|
||||
#if defined(__linux__)
|
||||
virtual std::vector<PulseRecordingStream> getOutput();
|
||||
virtual std::vector<PulsePlaybackStream> getPlayback();
|
||||
|
||||
void stopPassthrough();
|
||||
virtual std::optional<PulsePlaybackStream> startPassthrough(const std::string &);
|
||||
#else
|
||||
virtual std::vector<AudioDevice> getOutput();
|
||||
#endif
|
||||
|
||||
public:
|
||||
~Window();
|
||||
virtual void setup();
|
||||
virtual void mainLoop() = 0;
|
||||
|
||||
virtual void onError(const ErrorCode &) = 0;
|
||||
virtual void onSoundPlayed(const PlayingSound &) = 0;
|
||||
virtual void onSoundFinished(const PlayingSound &);
|
||||
virtual void onSoundProgressed(const PlayingSound &) = 0;
|
||||
virtual void onHotKeyReceived(const std::vector<int> &);
|
||||
|
||||
virtual void onEvent(const std::function<void()> &);
|
||||
};
|
||||
} // namespace Objects
|
||||
} // namespace Soundux
|