Merge branch 'development'

This commit is contained in:
D3SOX 2021-03-01 23:39:18 +01:00
commit db041674e6
No known key found for this signature in database
GPG Key ID: 14B89A87C228A740
84 changed files with 3207 additions and 3472 deletions

View File

@ -61,23 +61,6 @@ DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
- Regex: '.*'
Priority: 1
SortPriority: 0
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentCaseLabels: false
IndentGotoLabels: true
IndentPPDirectives: None
@ -104,7 +87,7 @@ PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 1000
PointerAlignment: Right
ReflowComments: true
SortIncludes: false
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false

View File

@ -48,4 +48,6 @@ Checks: "*,\
-google-readability-braces-around-statements,\
-cppcoreguidelines-non-private-member-variables-in-classes,\
-cppcoreguidelines-pro-type-union-access,\
-readability-static-accessed-through-instance"
-readability-static-accessed-through-instance,\
-cppcoreguidelines-special-member-functions,\
-readability-isolate-declaration"

View File

@ -2,14 +2,26 @@
Contributions are welcome! Here's how you can help:
- [Translating](#translations)
- [Contributing code](#code)
- [Reporting issues](#issues)
- [Donating](#donations)
- [Translations](#translations)
- [Code](#code)
- [Issues](#issues)
- [Donations](#donations)
## Translations
[Internationalization](https://github.com/Soundux/Soundux/issues/52) is not yet implemented. When we have a way to do this, we add the instructions here.
1. [Fork the frontend](https://github.com/Soundux/soundux-ui/fork) and [clone](https://help.github.com/articles/cloning-a-repository/) your fork.
2. Start translating!
- Add a translation file in `/src/locales/`
- If you are adding a translation which language doesn't exist yet, name your translation `[COUNTRY_CODE].js`
- If there already is a translation for your language and you want to add a territory specific one, name your translation `[COUNTRY_CODE]-[TERRITORY].js` so that it plays nicely with the [Implicit fallback](https://kazupon.github.io/vue-i18n/guide/fallback.html#implicit-fallback-using-locales)
- Replace `[COUNTRY_CODE]` with your corresponding code. [See the list here](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (Use column `639-1`)
- Replace `[TERRITORY]` with your corresponding territory code. [See the list here](https://en.wikipedia.org/wiki/ISO_3166-1#Officially_assigned_code_elements) (Use column `Alpha-2 code`)
- Add the corresponding translations for your language
3. Commit your changes to a new branch (not `master`, one change per branch) and push it:
- Use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716)
4. Once you are happy with your translation, submit a pull request.
## Code

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
ko_fi: soundux
ko_fi: soundux

View File

@ -7,6 +7,10 @@ assignees: ''
---
**Affected part of the program**
- [ ] Frontend <i>(e.g Something in the UI is not behaving correctly)</i>
- [ ] Backend <i>(e.g Crashes, Problems with Audio-Playback)</i>
**Describe the bug**
A clear and concise description of what the bug is.
@ -31,4 +35,4 @@ If applicable, add screenshots to help explain your problem.
- Version [version number or commit]
**Additional context**
Add any other context about the problem here.
Add any other context about the problem here.

View File

@ -9,3 +9,7 @@ updates:
directory: '/lib'
schedule:
interval: 'daily'
- package-ecosystem: 'gitsubmodule'
directory: '/src/ui/impl/webview/lib'
schedule:
interval: 'daily'

View File

@ -1,21 +1,19 @@
on:
push:
branches: [master]
branches: [ master ]
paths-ignore:
- "**/README.md"
- "**/docs/**"
- "**/release_windows.yml"
- "**/compile_windows.yml"
- '**/compile_linux.yml'
- '**/codeql-analysis-python.yml'
pull_request:
branches: [master]
branches: [ master ]
name: Build Flatpak
jobs:
flatpak-builder:
runs-on: ubuntu-20.04
container:
image: bilelmoussaoui/flatpak-github-actions:kde-5.15
image: bilelmoussaoui/flatpak-github-actions:gnome-3.38
options: --privileged
steps:
@ -23,4 +21,4 @@ jobs:
- uses: bilelmoussaoui/flatpak-github-actions@v2
with:
bundle: "soundux.flatpak"
manifest-path: "io.github.Soundux.yml"
manifest-path: "assets/flatpak/io.github.Soundux.yml"

View File

@ -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

View File

@ -3,10 +3,6 @@ on:
branches: [ master ]
paths-ignore:
- '**/README.md'
- '**/docs/**'
- '**/release_windows.yml'
- '**/build_flatpak.yml'
- '**/codeql-analysis-python.yml'
pull_request:
branches: [ master ]
@ -23,25 +19,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: 'true'
- name: Install Qt
uses: jurplel/install-qt-action@v2
with:
version: '5.15.2'
dir: '${{ github.workspace }}'
- name: Install other build dependencies
run: 'sudo apt-get install libx11-dev libxi-dev'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
submodules: recursive
- name: Install build dependencies
run: 'sudo apt-get install git build-essential cmake libx11-dev libxi-dev libwebkit2gtk-4.0-dev npm'
- name: Compile
run: 'mkdir build && cd build && cmake .. && make'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
run: 'mkdir build && cd build && cmake .. && cmake --build . --config Release'

33
.github/workflows/compile_windows.yml vendored Normal file
View 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

View File

@ -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
View File

@ -3,4 +3,4 @@ cmake-build-debug
.idea
.vscode
.history
src/qml/resources/*.js
.cache

19
.gitmodules vendored
View File

@ -4,6 +4,19 @@
[submodule "lib/miniaudio"]
path = lib/miniaudio
url = https://github.com/mackron/miniaudio
[submodule "lib/materialdesignicons"]
path = lib/materialdesignicons
url = https://github.com/Templarian/MaterialDesign-Webfont
[submodule "lib/fancypp"]
path = lib/fancypp
url = https://github.com/Curve/fancypp
[submodule "src/ui/impl/webview/lib/webview"]
path = src/ui/impl/webview/lib/webview
url = https://github.com/Soundux/webview
[submodule "lib/nativefiledialog-extended"]
path = lib/nativefiledialog-extended
url = https://github.com/btzy/nativefiledialog-extended/
[submodule "lib/InstanceGuard"]
path = lib/InstanceGuard
url = https://github.com/Grandbrain/InstanceGuard
[submodule "src/ui/impl/webview/lib/soundux-ui"]
path = src/ui/impl/webview/lib/soundux-ui
url = https://github.com/Soundux/soundux-ui/
branch = build

View File

@ -1,81 +1,51 @@
cmake_minimum_required(VERSION 3.1)
project(soundux VERSION 0.1.6 DESCRIPTION "A cross-platform soundboard in QtQuick")
project(soundux VERSION 1.0 DESCRIPTION "")
# Options
option(SOUNDUX_DEBUGINFO "Compiles with debug symbols to provide useful information for debugging" Off)
if (SOUNDUX_DEBUGINFO)
message("Compiling with debug info")
set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()
# -------
# Generate Material Design Js
if (NOT EXISTS "${CMAKE_SOURCE_DIR}/src/qml/resources/MaterialDesign.js")
if (MSVC)
execute_process(COMMAND cmd /c python "${CMAKE_SOURCE_DIR}/lib/generateMaterialDesignJs.py"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")
else()
execute_process(COMMAND python3 "${CMAKE_SOURCE_DIR}/lib/generateMaterialDesignJs.py"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")
endif()
message("Generated MaterialDesign.js")
endif()
# ---------------------------
# QT-Related
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt5 COMPONENTS Widgets Qml Quick QuickControls2 REQUIRED)
qt5_add_resources(QT_RESOURCES src/qml/qml.qrc)
# ----------
# Source Files to compile
file(GLOB src
"src/*.cpp"
"src/*.qml"
"src/*/*.cpp"
"src/*/*/*.cpp"
"src/*/*/*/*.cpp"
"src/*/*/*/*/*.cpp"
)
# -----------------------
# Linux Dependencies
if (UNIX AND NOT APPLE)
find_package(X11 REQUIRED)
if (WIN32)
add_executable(soundux WIN32 ${src})
else()
add_executable(soundux ${src})
endif()
# ------------------
# Unix Dependencies
if (UNIX)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
endif()
# ------------------
add_executable(soundux ${src} ${QT_RESOURCES})
target_include_directories(soundux PRIVATE ${Qt5Widgets_INCLUDE_DIRS} ${QtQml_INCLUDE_DIRS})
if (UNIX AND NOT APPLE)
target_include_directories(soundux PRIVATE ${X11_INCLUDE_DIR} ${X11_Xinput_INCLUDE_PATH})
endif()
target_include_directories(soundux PRIVATE "lib/json/single_include/nlohmann")
target_include_directories(soundux PRIVATE "lib/object_threadsafe")
target_include_directories(soundux PRIVATE "lib/miniaudio")
target_include_directories(soundux PRIVATE "lib/fancypp/include")
target_include_directories(soundux PRIVATE "lib/json/single_include")
target_include_directories(soundux PRIVATE "lib/InstanceGuard/Source")
target_compile_definitions(soundux PRIVATE ${Qt5Widgets_DEFINITIONS} ${QtQml_DEFINITIONS} ${Qt5Quick_DEFINITIONS})
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(soundux PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
if (UNIX AND NOT APPLE)
target_link_libraries(soundux ${X11_LIBRARIES} ${X11_Xinput_LIB} ${CMAKE_DL_LIBS})
endif()
if (UNIX)
target_link_libraries(soundux pthread)
find_package(X11 REQUIRED)
include_directories(${X11_INCLUDE_DIR})
target_link_libraries(soundux PRIVATE ${X11_LIBRARIES} ${X11_Xinput_LIB})
endif()
if (WIN32)
target_compile_definitions(soundux PRIVATE _CRT_SECURE_NO_WARNINGS=1 _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS=1 _UNICODE=1)
endif()
target_link_libraries(soundux
Qt5::Widgets
Qt5::Qml
Qt5::Quick
Qt5::QuickControls2
)
add_subdirectory(src/ui/impl/webview/lib/webview EXCLUDE_FROM_ALL)
add_subdirectory(lib/nativefiledialog-extended EXCLUDE_FROM_ALL)
add_subdirectory(lib/InstanceGuard/Source EXCLUDE_FROM_ALL)
target_link_libraries(soundux PUBLIC webview nfd InstanceGuard)
# [[ Build Frontend ]]
if (MSVC)
file(COPY "${CMAKE_SOURCE_DIR}/src/ui/impl/webview/lib/soundux-ui/"
DESTINATION "${CMAKE_SOURCE_DIR}/build/Release/dist")
else()
file(COPY "${CMAKE_SOURCE_DIR}/src/ui/impl/webview/lib/soundux-ui/"
DESTINATION "${CMAKE_SOURCE_DIR}/build/dist")
endif()
target_compile_features(soundux PRIVATE cxx_std_17)
set_target_properties(soundux PROPERTIES CMAKE_CXX_STANDARD 17)
@ -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 PROJECT_NAME ${PROJECT_NAME})
install(TARGETS soundux DESTINATION bin)
install(TARGETS soundux DESTINATION bin)
install(DIRECTORY "${CMAKE_SOURCE_DIR}/build/dist" DESTINATION bin)

View File

@ -1,6 +1,13 @@
![Soundux](https://socialify.git.ci/Soundux/Soundux/image?description=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2FSoundux%2FSoundux%2Fmaster%2Ficon.png&pattern=Plus&pulls=1&stargazers=1&theme=Dark)
<div align="center">
<p>
<img src="assets/logo.gif" height="200"/>
<br>
<h6>A cross-platform soundboard 🔊</h6>
<br>
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/Soundux/soundux?style=flat-square">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/Soundux/soundux?style=flat-square">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr-raw/Soundux/soundux?label=pulls&style=flat-square">
<br>
<a href="https://github.com/Soundux/Soundux/releases">
<img src="https://img.shields.io/github/release/Soundux/Soundux.svg?style=flat-square" alt="Latest Stable Release" />
</a>
@ -24,7 +31,10 @@
</div>
# Preview
![Dark Interface](https://raw.githubusercontent.com/Soundux/soundux.github.io/master/src/assets/screenshots/1.png)
| ![Dark Interface](assets/screenshots/1.png) | ![Seek/Pause/Stop Pane](assets/screenshots/2.png) |
| ---------------------------------------------------- | ------------------------------------------------- |
| ![Application Passthrough](assets/screenshots/3.png) | ![Search Pane](assets/screenshots/5.png) |
| ![Grid View](assets/screenshots/6.png) | ![Light Interface](assets/screenshots/7.png) |
# Introduction
Soundux is a cross-platform soundboard that features a simple user interface.
@ -37,8 +47,10 @@ These are required to run the program
Please refer to your distro instructions on how to install
- [pulseaudio](https://gitlab.freedesktop.org/pulseaudio/pulseaudio)
- Xorg
- Webkit2gtk
## Windows
- [VB-CABLE](https://vb-audio.com/Cable/) (Our installer automatically installs VB-CABLE)
- [Webview2 Runtime](https://developer.microsoft.com/de-de/microsoft-edge/webview2/) (Is also shipped with the installer)
# Installation
@ -67,25 +79,17 @@ Download our installer or portable from [the latest release](https://github.com/
## Build Dependencies
### Linux
This list may not be accurate. Contact me if you find missing dependencies that I can update this list
- [qt5-base](https://github.com/qt/qtbase) >=5.15
- [qt5-tools](https://github.com/qt/qt5) >=5.15
- [qt5-quickcontrols2](https://github.com/qt/qtquickcontrols2) >=5.15
This list may not be accurate. Contact me if you find missing dependencies so that I can update this list
- Webkit2gtk
- X11 client-side development headers
<b>Qt >= 5.15 is strictly required!</b>
#### Ubuntu and derivatives
#### Debian/Ubuntu and derivatives
```sh
sudo apt install git build-essential cmake libx11-dev libqt5x11extras5-dev libxi-dev
sudo apt install git build-essential cmake libx11-dev libxi-dev libwebkit2gtk-4.0-dev
```
Ubuntu does not have Qt 5.15 in its repositories so you need to use their [Online Installer](https://www.qt.io/download-thank-you?hsLang=en) or [compile it from source](https://doc.qt.io/qt-5/build-sources.html#linux-x11)
### Windows
*(We highly recommend you to just download the latest release for windows since it has all its dependencies packed with it)*
To compile on windows you'll have to install qt (*make sure the the important qt-paths are in your system-path!*)
- [Qt](https://www.qt.io/download-thank-you?os=windows)
- Nuget
- MSVC
- CMake

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -8,7 +8,7 @@
<summary>A cross-platform soundboard in QtQuick</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<url type="homepage">https://soundux.github.io/</url>
<url type="homepage">https://soundux.rocks/</url>
<url type="donation">https://ko-fi.com/soundux/</url>
<url type="bugtracker">https://github.com/Soundux/Soundux/issues</url>
<url type="help">https://github.com/Soundux/Soundux/issues</url>
@ -57,6 +57,7 @@
<li>Changelogs on Flathub</li>
<li>General bugfixes and stability improvements</li>
<li>Only unload PulseAudio modules created by Soundux</li>
<li>Update stop button state</li>
<li>Prevent opening the program multiple times</li>
</ul>
</description>

View File

@ -1,7 +1,7 @@
app-id: io.github.Soundux
runtime: org.kde.Platform
runtime-version: '5.15'
sdk: org.kde.Sdk
runtime: org.gnome.Platform
runtime-version: '3.38'
sdk: org.gnome.Sdk
command: soundux
finish-args:
- --device=all
@ -16,3 +16,4 @@ modules:
sources:
- type: git
url: https://github.com/Soundux/Soundux.git
branch: master

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/logo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
assets/screenshots/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
assets/screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/screenshots/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
assets/screenshots/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
assets/screenshots/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
assets/screenshots/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

BIN
icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

1
lib/InstanceGuard Submodule

@ -0,0 +1 @@
Subproject commit 5e3efce2036d744c04f1b190536d7c57e567040d

1
lib/fancypp Submodule

@ -0,0 +1 @@
Subproject commit 19d0dc6c7dc63a6260a22b27ef5ac6eb2ffe0982

View File

@ -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")

@ -1 +1 @@
Subproject commit db78ac1d7716f56fc9f1b030b715f872f93964e4
Subproject commit b83fe5dbf2c1a64a766ca44dae0afd8095205adc

@ -1 +0,0 @@
Subproject commit ca547d7878316031d24a1dbe5a9078693bb17517

@ -0,0 +1 @@
Subproject commit fbd8480bd63b8b9d808e3206acccd1cb113bac8b

View File

@ -1,2 +0,0 @@
#include "bindings.h"
// this file is needed for qt (MOC)

View File

@ -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)

View File

@ -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
View 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

View 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

View File

@ -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 &currentTab = 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 &currentTab = 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;
}

View File

@ -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;

View 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
View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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();
}

View 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();
};

View 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

View 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

View 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

View 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

View File

@ -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;
}
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -1,175 +1,59 @@
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <qqml.h>
#include <qurl.h>
#include <string>
#include "core/global/globals.hpp"
#include "helper/exceptions/crashhandler.hpp"
#include "ui/impl/webview/webview.hpp"
#include <InstanceGuard.hpp>
#include <fancy.hpp>
#include "core/core.h"
#include "bindings/bindings.h"
#include "config/config.h"
#include "playback/global.h"
#include "runguard/runguard.h"
#ifdef _WIN32
#include "hotkeys/windows.h"
#include "playback/windows.h"
#if defined(_WIN32)
int __stdcall WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
#else
#ifdef __linux__
#include "hotkeys/linux.h"
#include "playback/linux.h"
#include <csignal>
#include <execinfo.h>
#else
// #include "hotkeys/mac.h"
int main()
#endif
#endif
#ifdef __linux__
void sigHandler(int signal)
{
if (signal == 8)
{
std::cerr << "This crash is probably related to a bad pulseaudio config" << std::endl;
}
std::cerr << "Received Signal: " << signal << std::endl;
std::cerr << "Backtrace available" << std::endl;
void *elements[20];
auto size = backtrace(elements, 20);
auto *stack = backtrace_symbols(elements, size);
for (int i = 0; size > i; i++)
{
std::cerr << stack[i] << std::endl;
}
free(stack);
exit(1);
}
#if defined(_WIN32)
DWORD lMode;
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleMode(hStdout, &lMode);
SetConsoleMode(hStdout, lMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
#endif
void exceptionHandler()
{
std::cerr << "An exception caused the program to crash" << std::endl;
auto exception = std::current_exception();
CrashHandler::init();
try
InstanceGuard::InstanceGuard guard("soundux-guard");
if (guard.IsAnotherInstanceRunning())
{
std::rethrow_exception(exception);
}
catch (std::exception &e)
{
std::cerr << "Exception: " << e.what() << std::endl;
std::cerr << "Exception Type: " << typeid(e).name() << std::endl;
}
catch (...)
{
std::cerr << "Unknown exception" << std::endl;
Fancy::fancy.logTime().failure() << "Another Instance is already running!" << std::endl;
return 1;
}
#ifdef __linux__
std::cerr << "Backtrace available" << std::endl;
void *elements[20];
auto size = backtrace(elements, 20);
auto *stack = backtrace_symbols(elements, size);
for (int i = 0; size > i; i++)
#if defined(__linux__)
if (!Soundux::Globals::gPulse.isSwitchOnConnectLoaded())
{
std::cerr << stack[i] << std::endl;
Soundux::Globals::gPulse.setup();
}
free(stack);
Soundux::Globals::gAudio.setup();
#endif
}
int main(int argc, char **argv)
{
Soundux::RunGuard guard("soundux");
if (!guard.tryToRun())
Soundux::Globals::gConfig.load();
#if defined(__linux__)
if (Soundux::Globals::gConfig.settings.useAsDefaultDevice)
{
std::cerr << "Soundux is already running!";
return 0;
}
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#ifdef _WIN32
::ShowWindow(::GetConsoleWindow(), SW_HIDE);
std::vector<char *> args(argv, argv + argc);
// Thanks to this article https://kb.froglogic.com/squish/qt/howto/automating-native-file-dialogs/ !
args.push_back(const_cast<char *>("-platformtheme"));
args.push_back(const_cast<char *>("none"));
argc = args.size();
argv = args.data();
QApplication app(argc, (char **)args.data());
#else
QApplication app(argc, argv);
#endif
QQmlApplicationEngine engine;
QQuickStyle::setStyle("Material");
app.setOrganizationDomain("https://github.com/D3SOX/Soundux");
app.setOrganizationName("Soundux");
Soundux::Config::loadConfig();
Soundux::Playback::usedDevices = Soundux::Config::gConfig.volumes;
gCore.setEngine(&engine);
engine.rootContext()->setContextProperty("core", &gCore);
// register meta types
qRegisterMetaType<QTab>();
qRegisterMetaType<std::vector<QTab>>();
qRegisterMetaType<QSound>();
qRegisterMetaType<std::vector<QSound>>();
#ifdef __linux__
qRegisterMetaType<QPulseAudioRecordingStream>();
qRegisterMetaType<std::vector<QPulseAudioRecordingStream>>();
#endif
Soundux::Hooks::setup();
std::set_terminate(exceptionHandler);
#ifdef __linux__
signal(SIGSEGV, sigHandler);
signal(SIGABRT, sigHandler);
signal(SIGFPE, sigHandler);
Soundux::Playback::createSink();
for (const auto &device : Soundux::Playback::getPlaybackDevices())
{
if (device.name == Soundux::Playback::internal::sinkName)
{
gCore.setLinuxSink(device);
break;
}
Soundux::Globals::gPulse.setDefaultSourceToSoundboardSink();
}
#endif
Soundux::Globals::gData = Soundux::Globals::gConfig.data;
Soundux::Globals::gSettings = Soundux::Globals::gConfig.settings;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
static_cast<void>(app.exec());
Soundux::Globals::gGui = std::make_unique<Soundux::Objects::WebView>();
Soundux::Globals::gGui->setup();
Soundux::Globals::gGui->mainLoop();
Soundux::Config::gConfig.volumes = Soundux::Playback::usedDevices;
Soundux::Config::saveConfig();
Soundux::Hooks::stop();
Soundux::Playback::stopAllAudio();
#ifdef __linux__
Soundux::Playback::deleteSink();
Soundux::Globals::gAudio.destory();
#if defined(__linux__)
Soundux::Globals::gPulse.destroy();
#endif
Soundux::Playback::destroy();
Soundux::Globals::gConfig.data = Soundux::Globals::gData;
Soundux::Globals::gConfig.settings = Soundux::Globals::gSettings;
Soundux::Globals::gConfig.save();
return 0;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 === "")
}
}
}
}

View File

@ -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"
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -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

@ -0,0 +1 @@
Subproject commit 5e528dfeff2e93953de70cd25912b6a64a5c2a18

@ -0,0 +1 @@
Subproject commit 05f3d12be44a21ccaa4753d2d68adbd3fb726a09

View 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 &param) -> std::string {
return nlohmann::json(Globals::gAudio.getPlayingSounds()).dump();
},
true);
webview->addCallback(
"getSettings",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> std::string {
return nlohmann::json(Globals::gSettings).dump();
},
true);
webview->addCallback(
"getData",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> std::string {
return nlohmann::json(Globals::gData).dump();
},
true);
webview->addCallback(
"addTab",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> 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 &param) -> 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 &param) -> std::string {
if (stopSound(std::stoi(param[0])))
{
return "true";
}
return "false";
});
webview->addCallback("stopSounds",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> std::string {
stopSounds();
return "";
});
webview->addCallback(
"pauseSound",
[this]([[maybe_unused]] auto &wv, const auto &param) -> 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 &param) -> 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 &param) -> 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 &param) -> 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 &param) -> 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 &param) -> 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 &param) -> 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 &param) -> std::string {
return nlohmann::json(removeTab(std::stoi(param[0]))).dump();
},
true);
webview->addCallback(
"refreshTab",
[this]([[maybe_unused]] auto &wv, const auto &param) -> 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 &param) -> 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 &param) -> 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 &param) -> 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 &param) -> std::string {
return nlohmann::json(getOutput()).dump();
},
true);
#if defined(_WIN32)
webview->addCallback("openUrl", [this]([[maybe_unused]] auto &wv, const auto &param) -> 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 &param) -> std::string {
system(("xdg-open \"" + param[0] + "\"").c_str());
return "";
});
webview->addCallback(
"getOutput",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> std::string {
return nlohmann::json(getOutput()).dump();
},
true);
webview->addCallback(
"getPlayback",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> std::string {
return nlohmann::json(getPlayback()).dump();
},
true);
webview->addCallback(
"startPassthrough",
[this]([[maybe_unused]] auto &wv, const auto &param) -> 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 &param) -> std::string {
stopPassthrough();
return "";
});
webview->addCallback(
"isSwitchOnConnectLoaded",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> std::string {
return Globals::gPulse.isSwitchOnConnectLoaded() ? "true" : "false";
},
true);
webview->addCallback("unloadSwitchOnConnect",
[this]([[maybe_unused]] auto &wv, [[maybe_unused]] const auto &param) -> 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

View 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
View 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
View 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