Merge branch 'development'
@ -61,23 +61,6 @@ DerivePointerAlignment: false
|
|||||||
DisableFormat: false
|
DisableFormat: false
|
||||||
ExperimentalAutoDetectBinPacking: false
|
ExperimentalAutoDetectBinPacking: false
|
||||||
FixNamespaceComments: true
|
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
|
IndentCaseLabels: false
|
||||||
IndentGotoLabels: true
|
IndentGotoLabels: true
|
||||||
IndentPPDirectives: None
|
IndentPPDirectives: None
|
||||||
@ -104,7 +87,7 @@ PenaltyExcessCharacter: 1000000
|
|||||||
PenaltyReturnTypeOnItsOwnLine: 1000
|
PenaltyReturnTypeOnItsOwnLine: 1000
|
||||||
PointerAlignment: Right
|
PointerAlignment: Right
|
||||||
ReflowComments: true
|
ReflowComments: true
|
||||||
SortIncludes: false
|
SortIncludes: true
|
||||||
SortUsingDeclarations: true
|
SortUsingDeclarations: true
|
||||||
SpaceAfterCStyleCast: false
|
SpaceAfterCStyleCast: false
|
||||||
SpaceAfterLogicalNot: false
|
SpaceAfterLogicalNot: false
|
||||||
|
@ -48,4 +48,6 @@ Checks: "*,\
|
|||||||
-google-readability-braces-around-statements,\
|
-google-readability-braces-around-statements,\
|
||||||
-cppcoreguidelines-non-private-member-variables-in-classes,\
|
-cppcoreguidelines-non-private-member-variables-in-classes,\
|
||||||
-cppcoreguidelines-pro-type-union-access,\
|
-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:
|
Contributions are welcome! Here's how you can help:
|
||||||
|
|
||||||
- [Translating](#translations)
|
- [Translations](#translations)
|
||||||
- [Contributing code](#code)
|
- [Code](#code)
|
||||||
- [Reporting issues](#issues)
|
- [Issues](#issues)
|
||||||
- [Donating](#donations)
|
- [Donations](#donations)
|
||||||
|
|
||||||
## Translations
|
## 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
|
## Code
|
||||||
|
|
||||||
|
2
.github/FUNDING.yml
vendored
@ -1 +1 @@
|
|||||||
ko_fi: soundux
|
ko_fi: soundux
|
6
.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**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
@ -31,4 +35,4 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
- Version [version number or commit]
|
- Version [version number or commit]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
4
.github/dependabot.yml
vendored
@ -9,3 +9,7 @@ updates:
|
|||||||
directory: '/lib'
|
directory: '/lib'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/README.md"
|
- "**/README.md"
|
||||||
- "**/docs/**"
|
- "**/compile_windows.yml"
|
||||||
- "**/release_windows.yml"
|
|
||||||
- '**/compile_linux.yml'
|
- '**/compile_linux.yml'
|
||||||
- '**/codeql-analysis-python.yml'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [ master ]
|
||||||
|
|
||||||
name: Build Flatpak
|
name: Build Flatpak
|
||||||
jobs:
|
jobs:
|
||||||
flatpak-builder:
|
flatpak-builder:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
container:
|
container:
|
||||||
image: bilelmoussaoui/flatpak-github-actions:kde-5.15
|
image: bilelmoussaoui/flatpak-github-actions:gnome-3.38
|
||||||
options: --privileged
|
options: --privileged
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -23,4 +21,4 @@ jobs:
|
|||||||
- uses: bilelmoussaoui/flatpak-github-actions@v2
|
- uses: bilelmoussaoui/flatpak-github-actions@v2
|
||||||
with:
|
with:
|
||||||
bundle: "soundux.flatpak"
|
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 ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**/README.md'
|
- '**/README.md'
|
||||||
- '**/docs/**'
|
|
||||||
- '**/release_windows.yml'
|
|
||||||
- '**/build_flatpak.yml'
|
|
||||||
- '**/codeql-analysis-python.yml'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
@ -23,25 +19,8 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: recursive
|
||||||
|
- name: Install build dependencies
|
||||||
- name: Install Qt
|
run: 'sudo apt-get install git build-essential cmake libx11-dev libxi-dev libwebkit2gtk-4.0-dev npm'
|
||||||
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 }}
|
|
||||||
|
|
||||||
- name: Compile
|
- name: Compile
|
||||||
run: 'mkdir build && cd build && cmake .. && make'
|
run: 'mkdir build && cd build && cmake .. && cmake --build . --config Release'
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
|
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
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.history
|
.history
|
||||||
src/qml/resources/*.js
|
.cache
|
19
.gitmodules
vendored
@ -4,6 +4,19 @@
|
|||||||
[submodule "lib/miniaudio"]
|
[submodule "lib/miniaudio"]
|
||||||
path = lib/miniaudio
|
path = lib/miniaudio
|
||||||
url = https://github.com/mackron/miniaudio
|
url = https://github.com/mackron/miniaudio
|
||||||
[submodule "lib/materialdesignicons"]
|
[submodule "lib/fancypp"]
|
||||||
path = lib/materialdesignicons
|
path = lib/fancypp
|
||||||
url = https://github.com/Templarian/MaterialDesign-Webfont
|
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)
|
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
|
file(GLOB src
|
||||||
"src/*.cpp"
|
"src/*.cpp"
|
||||||
"src/*.qml"
|
|
||||||
"src/*/*.cpp"
|
"src/*/*.cpp"
|
||||||
|
"src/*/*/*.cpp"
|
||||||
|
"src/*/*/*/*.cpp"
|
||||||
|
"src/*/*/*/*/*.cpp"
|
||||||
)
|
)
|
||||||
# -----------------------
|
|
||||||
|
|
||||||
# Linux Dependencies
|
if (WIN32)
|
||||||
if (UNIX AND NOT APPLE)
|
add_executable(soundux WIN32 ${src})
|
||||||
find_package(X11 REQUIRED)
|
else()
|
||||||
|
add_executable(soundux ${src})
|
||||||
endif()
|
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/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)
|
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()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(soundux
|
add_subdirectory(src/ui/impl/webview/lib/webview EXCLUDE_FROM_ALL)
|
||||||
Qt5::Widgets
|
add_subdirectory(lib/nativefiledialog-extended EXCLUDE_FROM_ALL)
|
||||||
Qt5::Qml
|
add_subdirectory(lib/InstanceGuard/Source EXCLUDE_FROM_ALL)
|
||||||
Qt5::Quick
|
target_link_libraries(soundux PUBLIC webview nfd InstanceGuard)
|
||||||
Qt5::QuickControls2
|
|
||||||
)
|
# [[ 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)
|
target_compile_features(soundux PRIVATE cxx_std_17)
|
||||||
set_target_properties(soundux PROPERTIES CMAKE_CXX_STANDARD 17)
|
set_target_properties(soundux PROPERTIES CMAKE_CXX_STANDARD 17)
|
||||||
@ -85,4 +55,5 @@ set_target_properties(soundux PROPERTIES CMAKE_CXX_STANDARD_REQUIRED On)
|
|||||||
set_target_properties(soundux PROPERTIES VERSION ${PROJECT_VERSION})
|
set_target_properties(soundux PROPERTIES VERSION ${PROJECT_VERSION})
|
||||||
set_target_properties(soundux PROPERTIES PROJECT_NAME ${PROJECT_NAME})
|
set_target_properties(soundux PROPERTIES PROJECT_NAME ${PROJECT_NAME})
|
||||||
|
|
||||||
install(TARGETS soundux DESTINATION bin)
|
install(TARGETS soundux DESTINATION bin)
|
||||||
|
install(DIRECTORY "${CMAKE_SOURCE_DIR}/build/dist" DESTINATION bin)
|
34
README.md
@ -1,6 +1,13 @@
|
|||||||

|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p>
|
<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">
|
<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" />
|
<img src="https://img.shields.io/github/release/Soundux/Soundux.svg?style=flat-square" alt="Latest Stable Release" />
|
||||||
</a>
|
</a>
|
||||||
@ -24,7 +31,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Preview
|
# Preview
|
||||||

|
|  |  |
|
||||||
|
| ---------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
|  |  |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
Soundux is a cross-platform soundboard that features a simple user interface.
|
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
|
Please refer to your distro instructions on how to install
|
||||||
- [pulseaudio](https://gitlab.freedesktop.org/pulseaudio/pulseaudio)
|
- [pulseaudio](https://gitlab.freedesktop.org/pulseaudio/pulseaudio)
|
||||||
- Xorg
|
- Xorg
|
||||||
|
- Webkit2gtk
|
||||||
## Windows
|
## Windows
|
||||||
- [VB-CABLE](https://vb-audio.com/Cable/) (Our installer automatically installs VB-CABLE)
|
- [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
|
# Installation
|
||||||
|
|
||||||
@ -67,25 +79,17 @@ Download our installer or portable from [the latest release](https://github.com/
|
|||||||
## Build Dependencies
|
## Build Dependencies
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
This list may not be accurate. Contact me if you find missing dependencies that I can update this list
|
This list may not be accurate. Contact me if you find missing dependencies so that I can update this list
|
||||||
- [qt5-base](https://github.com/qt/qtbase) >=5.15
|
- Webkit2gtk
|
||||||
- [qt5-tools](https://github.com/qt/qt5) >=5.15
|
|
||||||
- [qt5-quickcontrols2](https://github.com/qt/qtquickcontrols2) >=5.15
|
|
||||||
- X11 client-side development headers
|
- X11 client-side development headers
|
||||||
|
|
||||||
<b>Qt >= 5.15 is strictly required!</b>
|
#### Debian/Ubuntu and derivatives
|
||||||
|
|
||||||
#### Ubuntu and derivatives
|
|
||||||
```sh
|
```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
|
### Windows
|
||||||
*(We highly recommend you to just download the latest release for windows since it has all its dependencies packed with it)*
|
- Nuget
|
||||||
|
|
||||||
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)
|
|
||||||
- MSVC
|
- MSVC
|
||||||
- CMake
|
- 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>
|
<summary>A cross-platform soundboard in QtQuick</summary>
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0-only</project_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="donation">https://ko-fi.com/soundux/</url>
|
||||||
<url type="bugtracker">https://github.com/Soundux/Soundux/issues</url>
|
<url type="bugtracker">https://github.com/Soundux/Soundux/issues</url>
|
||||||
<url type="help">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>Changelogs on Flathub</li>
|
||||||
<li>General bugfixes and stability improvements</li>
|
<li>General bugfixes and stability improvements</li>
|
||||||
<li>Only unload PulseAudio modules created by Soundux</li>
|
<li>Only unload PulseAudio modules created by Soundux</li>
|
||||||
|
<li>Update stop button state</li>
|
||||||
<li>Prevent opening the program multiple times</li>
|
<li>Prevent opening the program multiple times</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
@ -1,7 +1,7 @@
|
|||||||
app-id: io.github.Soundux
|
app-id: io.github.Soundux
|
||||||
runtime: org.kde.Platform
|
runtime: org.gnome.Platform
|
||||||
runtime-version: '5.15'
|
runtime-version: '3.38'
|
||||||
sdk: org.kde.Sdk
|
sdk: org.gnome.Sdk
|
||||||
command: soundux
|
command: soundux
|
||||||
finish-args:
|
finish-args:
|
||||||
- --device=all
|
- --device=all
|
||||||
@ -16,3 +16,4 @@ modules:
|
|||||||
sources:
|
sources:
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/Soundux/Soundux.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 "core/global/globals.hpp"
|
||||||
#include <QQmlApplicationEngine>
|
#include "helper/exceptions/crashhandler.hpp"
|
||||||
#include <QQmlContext>
|
#include "ui/impl/webview/webview.hpp"
|
||||||
#include <QQuickStyle>
|
#include <InstanceGuard.hpp>
|
||||||
#include <exception>
|
#include <fancy.hpp>
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
|
||||||
#include <qqml.h>
|
|
||||||
#include <qurl.h>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "core/core.h"
|
#if defined(_WIN32)
|
||||||
#include "bindings/bindings.h"
|
int __stdcall WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||||
#include "config/config.h"
|
|
||||||
#include "playback/global.h"
|
|
||||||
#include "runguard/runguard.h"
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
#include "hotkeys/windows.h"
|
|
||||||
#include "playback/windows.h"
|
|
||||||
#else
|
#else
|
||||||
#ifdef __linux__
|
int main()
|
||||||
#include "hotkeys/linux.h"
|
|
||||||
#include "playback/linux.h"
|
|
||||||
#include <csignal>
|
|
||||||
#include <execinfo.h>
|
|
||||||
#else
|
|
||||||
// #include "hotkeys/mac.h"
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
void sigHandler(int signal)
|
|
||||||
{
|
{
|
||||||
if (signal == 8)
|
#if defined(_WIN32)
|
||||||
{
|
DWORD lMode;
|
||||||
std::cerr << "This crash is probably related to a bad pulseaudio config" << std::endl;
|
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
}
|
GetConsoleMode(hStdout, &lMode);
|
||||||
|
SetConsoleMode(hStdout, lMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
|
||||||
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);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void exceptionHandler()
|
CrashHandler::init();
|
||||||
{
|
|
||||||
std::cerr << "An exception caused the program to crash" << std::endl;
|
|
||||||
auto exception = std::current_exception();
|
|
||||||
|
|
||||||
try
|
InstanceGuard::InstanceGuard guard("soundux-guard");
|
||||||
|
if (guard.IsAnotherInstanceRunning())
|
||||||
{
|
{
|
||||||
std::rethrow_exception(exception);
|
Fancy::fancy.logTime().failure() << "Another Instance is already running!" << std::endl;
|
||||||
}
|
return 1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
#if defined(__linux__)
|
||||||
std::cerr << "Backtrace available" << std::endl;
|
if (!Soundux::Globals::gPulse.isSwitchOnConnectLoaded())
|
||||||
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;
|
Soundux::Globals::gPulse.setup();
|
||||||
}
|
}
|
||||||
free(stack);
|
Soundux::Globals::gAudio.setup();
|
||||||
#endif
|
#endif
|
||||||
}
|
Soundux::Globals::gConfig.load();
|
||||||
|
#if defined(__linux__)
|
||||||
int main(int argc, char **argv)
|
if (Soundux::Globals::gConfig.settings.useAsDefaultDevice)
|
||||||
{
|
|
||||||
Soundux::RunGuard guard("soundux");
|
|
||||||
if (!guard.tryToRun())
|
|
||||||
{
|
{
|
||||||
std::cerr << "Soundux is already running!";
|
Soundux::Globals::gPulse.setDefaultSourceToSoundboardSink();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
Soundux::Globals::gData = Soundux::Globals::gConfig.data;
|
||||||
|
Soundux::Globals::gSettings = Soundux::Globals::gConfig.settings;
|
||||||
|
|
||||||
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
|
Soundux::Globals::gGui = std::make_unique<Soundux::Objects::WebView>();
|
||||||
static_cast<void>(app.exec());
|
Soundux::Globals::gGui->setup();
|
||||||
|
Soundux::Globals::gGui->mainLoop();
|
||||||
|
|
||||||
Soundux::Config::gConfig.volumes = Soundux::Playback::usedDevices;
|
Soundux::Globals::gAudio.destory();
|
||||||
Soundux::Config::saveConfig();
|
#if defined(__linux__)
|
||||||
|
Soundux::Globals::gPulse.destroy();
|
||||||
Soundux::Hooks::stop();
|
|
||||||
|
|
||||||
Soundux::Playback::stopAllAudio();
|
|
||||||
#ifdef __linux__
|
|
||||||
Soundux::Playback::deleteSink();
|
|
||||||
#endif
|
#endif
|
||||||
Soundux::Playback::destroy();
|
Soundux::Globals::gConfig.data = Soundux::Globals::gData;
|
||||||
|
Soundux::Globals::gConfig.settings = Soundux::Globals::gSettings;
|
||||||
|
Soundux::Globals::gConfig.save();
|
||||||
|
|
||||||
return 0;
|
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
|