diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..93a7931 --- /dev/null +++ b/.clang-format @@ -0,0 +1,35 @@ +# clang-format configuration for visualization.matrix +# Based on LLVM style with custom adjustments + +BasedOnStyle: LLVM +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +BreakBeforeBraces: Allman +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +IndentCaseLabels: true +ColumnLimit: 100 +AllowAllParametersOfDeclarationOnNextLine: true +AllowAllArgumentsOnNextLine: true +SpacesBeforeTrailingComments: 2 +Cpp11BracedListStyle: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +ContinuationIndentWidth: 2 +BinPackParameters: false +BinPackArguments: false +PenaltyBreakBeforeFirstCallParameter: 10000 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000 +PenaltyReturnTypeOnItsOwnLine: 200 + +# C++ specific settings +Standard: Cpp17 +DerivePointerAlignment: true +PointerAlignment: Right +ReferenceAlignment: Right + +# OpenGL/GLSL specific +# Keep GLSL shader code formatting simple diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..66fb751 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Git directory +.git +.gitignore + +# Build artifacts +build/ +*.o +*.a +*.so +*.dll +*.exe +*.lib +*.dylib + +# CMake files +CMakeCache.txt +CMakeFiles/ + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS specific files +.DS_Store +.AppleDouble +.LSOverride +Thumbs.db + +# Documentation (not needed in image) +*.md +LICENSE* +CHANGELOG* +README* +CONTRIBUTING* +CODE_OF_CONDUCT* +SECURITY* +DEPENDENCY_POLICY* + +# GitHub specific files +.github/ +.gitattributes + +# Editor files +*.project +*.cproject +.classpath +.settings/ + +# Debian build files +debian/ +*.deb + +# Kodi specific +xbmc/ +kodi/ + +# Addon output +visualization.matrix/addon.xml +*.zip + +# Logs +*.log + +# Temporary files +tmp/ +temp/ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..93499d0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,19 @@ +# Code Owners for visualization.matrix + +# Default owners for the entire repository +* @MarcelRaschke + +# Specific paths +/src/ @MarcelRaschke +/lib/ @MarcelRaschke +/visualization.matrix/ @MarcelRaschke + +# Documentation +*.md @MarcelRaschke + +# GitHub Workflows +.github/workflows/ @MarcelRaschke + +# Security files +SECURITY.md @MarcelRaschke +CONTRIBUTING.md @MarcelRaschke diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ed346d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug Report +about: Create a report to help us improve +labels: bug +assignees: '' +--- + +### 🐛 Bug Description +A clear and concise description of what the bug is. + +### 🔍 Steps to Reproduce +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +### 📋 Expected Behavior +A clear description of what you expected to happen. + +### 🖥️ Environment +- **Kodi Version**: [e.g., 20.0] +- **Operating System**: [e.g., Ubuntu 22.04, Windows 11] +- **Addon Version**: [e.g., 1.0.0] +- **Hardware**: [e.g., NVIDIA GTX 1080, Intel i7-12700K] + +### 📝 Additional Context +Add any other context about the problem here (e.g., logs, screenshots). + +### 📎 Attachments +- [ ] Screenshot +- [ ] Log file (`kodi.log`) +- [ ] Crash report + +--- + +**Note**: For **security vulnerabilities**, please follow the instructions in **[SECURITY.md](SECURITY.md)**. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..0f0e838 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature Request +about: Suggest an idea for this project +labels: enhancement +assignees: '' +--- + +### ✨ Feature Description +A clear and concise description of the feature you would like to see. + +### 🎯 Use Case +Describe the **use case** for this feature. Why is it needed? + +### 💡 Proposed Solution +If you have a specific implementation in mind, describe it here. + +### 📋 Alternatives Considered +Have you considered any alternative solutions or workarounds? + +### 📎 Additional Context +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/security_vulnerability.md b/.github/ISSUE_TEMPLATE/security_vulnerability.md new file mode 100644 index 0000000..76025ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_vulnerability.md @@ -0,0 +1,17 @@ +--- +name: Security Vulnerability Report +about: Report a security vulnerability in visualization.matrix +labels: security, vulnerability +assignees: '' +--- + +**⚠️ DO NOT USE THIS FORM FOR PUBLIC SECURITY VULNERABILITIES!** + +If you have discovered a **security vulnerability**, please **do not** open a public issue. +Instead, follow the instructions in **[SECURITY.md](https://github.com/MarcelRaschke/visualization.matrix/blob/master/SECURITY.md)**. + +--- + +### For Non-Security Bugs + +If this is a **non-security bug**, please use the [Bug Report](https://github.com/MarcelRaschke/visualization.matrix/issues/new/choose) template instead. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a7cb9fd --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ +## 📌 Pull Request Description + +**Please fill out the following sections to help reviewers understand your changes.** + +--- + +### ✅ What does this PR do? +*A clear and concise description of the changes.* + +### 🎯 Why is this change needed? +*Explain the motivation and context for this PR.* + +### 🔗 Related Issues or PRs +*Link to any related issues (e.g., `Closes #123`) or pull requests.* + +--- + +## 📝 Checklist + +- [ ] **Code**: Follows the [coding standards](CONTRIBUTING.md#coding-standards). +- [ ] **Tests**: All existing tests pass (if applicable). +- [ ] **Security**: No hardcoded secrets or sensitive data. +- [ ] **Documentation**: Updated (if applicable). +- [ ] **Build**: Successfully builds on Linux/Windows. + +--- + +## 🛡️ Security Considerations + +- [ ] This PR does **not** introduce new dependencies. +- [ ] This PR does **not** modify security-critical code (e.g., authentication, encryption). +- [ ] This PR has been **scanned** with: + - [ ] CodeQL + - [ ] clang-tidy + - [ ] cppcheck + - [ ] Trivy + +If this PR introduces **security-relevant changes**, please describe them below: + +--- + +## 📎 Additional Context +*Add any other context, screenshots, or references here.* diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9706cc9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "security" + + - package-ecosystem: "cmake" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "security" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..5bc946c --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,60 @@ +# GitHub Labeler Configuration + +# Labels to apply based on file paths +labels: + - name: "security" + color: "d73a4a" + description: "Security-related changes or vulnerabilities" + files: + - SECURITY.md + - .github/workflows/codeql.yml + - .github/workflows/trivy.yml + - .github/workflows/fuzzing.yml + + - name: "documentation" + color: "0075ca" + description: "Changes to documentation" + files: + - README.md + - CONTRIBUTING.md + - LICENSE.md + - .github/ISSUE_TEMPLATE/* + - .github/PULL_REQUEST_TEMPLATE.md + + - name: "ci/cd" + color: "000000" + description: "Changes to CI/CD pipelines" + files: + - .github/workflows/* + - azure-pipelines.yml + - .travis.yml + - appveyor.yml + + - name: "bug" + color: "d73a4a" + description: "Bug fixes" + files: + - src/* + - lib/* + + - name: "enhancement" + color: "a2eeef" + description: "New features or improvements" + files: + - src/* + - lib/* + + - name: "dependencies" + color: "0366d6" + description: "Dependency updates" + files: + - CMakeLists.txt + - depends/* + + - name: "build" + color: "000000" + description: "Build-related changes" + files: + - CMakeLists.txt + - Find*.cmake + - debian/* diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml new file mode 100644 index 0000000..5340899 --- /dev/null +++ b/.github/workflows/cache.yml @@ -0,0 +1,77 @@ +name: Cache Management + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - 'CMakeLists.txt' + - 'src/**' + - 'lib/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - 'CMakeLists.txt' + - 'src/**' + - 'lib/**' + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # Run every Sunday at midnight + +jobs: + cleanup-cache: + name: Cleanup Cache + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cleanup old caches + uses: actions/github-script@v6 + with: + script: | + const caches = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const now = new Date(); + const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + + for (const cache of caches.data.actions_caches) { + const cacheDate = new Date(cache.created_at); + if (cacheDate < oneWeekAgo) { + console.log(`Deleting cache ${cache.id} created at ${cache.created_at}`); + await github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }); + } + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + prewarm-cache: + name: Prewarm Cache + runs-on: ubuntu-latest + needs: cleanup-cache + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache CMake dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cache/CMake + key: prewarm-cmake-${{ hashFiles('CMakeLists.txt') }} + restore-keys: | + prewarm-cmake- + + - name: Install and cache build dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake g++ make libgl1-mesa-dev libglu1-mesa-dev ninja-build + echo "Dependencies installed and cached" diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..47e9ac6 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,116 @@ +name: Auto-generate CHANGELOG + +on: + push: + branches: [ master, Matrix, Nexus ] + tags: [ 'v*' ] + pull_request: + branches: [ master, Matrix, Nexus ] + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 1.0.0)' + required: true + dry-run: + description: 'Dry run (do not create commit)' + required: false + default: 'false' + +jobs: + generate-changelog: + name: Generate CHANGELOG + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required to get full git history + + - name: Install standard-version + run: | + npm install -g standard-version + + - name: Generate CHANGELOG + id: changelog + run: | + # Configure git + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + + # Generate changelog using standard-version + VERSION=${{ github.event.inputs.version || 'patch' }} + DRY_RUN=${{ github.event.inputs.dry-run || 'false' }} + + if [ "$DRY_RUN" = "true" ]; then + npx standard-version --dry-run --no-verify --skip.changelog + else + # Generate changelog without committing + npx standard-version --no-verify --skip.commit --skip.tag --skip.changelog + + # Manually update CHANGELOG.md + if [ -f "CHANGELOG.md" ]; then + # Extract version and date from git log + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") + CURRENT_DATE=$(date +"%Y-%m-%d") + + # Generate changelog entries from commits since last tag + echo "## [$VERSION] - $CURRENT_DATE" >> CHANGELOG_NEW.md + echo "" >> CHANGELOG_NEW.md + + # Get commits since last tag + git log $LATEST_TAG..HEAD --pretty=format:"- %s (%h)" --reverse >> CHANGELOG_NEW.md + echo "" >> CHANGELOG_NEW.md + + # Prepend to existing CHANGELOG.md + cat CHANGELOG_NEW.md CHANGELOG.md > CHANGELOG_TEMP.md + mv CHANGELOG_TEMP.md CHANGELOG.md + rm CHANGELOG_NEW.md + fi + fi + + - name: Create Pull Request with CHANGELOG updates + if: github.event.inputs.dry-run != 'true' && github.event_name != 'push' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "docs: update CHANGELOG for v${{ github.event.inputs.version }}" + title: "docs: update CHANGELOG for v${{ github.event.inputs.version }}" + body: "Automatically generated CHANGELOG updates for version ${{ github.event.inputs.version }}" + branch: "changelog/update-v${{ github.event.inputs.version }}" + delete-branch: true + path: . + reviewers: MarcelRaschke + + - name: Upload CHANGELOG artifact + uses: actions/upload-artifact@v4 + with: + name: changelog + path: CHANGELOG.md + retention-days: 30 + + release: + name: Create Release + needs: generate-changelog + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download CHANGELOG artifact + uses: actions/download-artifact@v4 + with: + name: changelog + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + CHANGELOG.md + body_file: CHANGELOG.md + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 0000000..bac199d --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,74 @@ +name: Code Formatting + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + workflow_dispatch: + +jobs: + clang-format-check: + name: clang-format Check + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Install clang-format + run: | + sudo apt-get update + sudo apt-get install -y clang-format-14 + + - name: Check code formatting + run: | + # Find all C++ files + find . -name "*.cpp" -o -name "*.h" -o -name "*.c" | grep -v ".git" | while read file; do + if ! clang-format-14 --dry-run --Werror "$file"; then + echo "::error file=$file::Code formatting issues found" + exit 1 + fi + done + + clang-format-apply: + name: clang-format Apply + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install clang-format + run: | + sudo apt-get update + sudo apt-get install -y clang-format-14 + + - name: Apply clang-format + run: | + # Find and format all C++ files + find . -name "*.cpp" -o -name "*.h" -o -name "*.c" | grep -v ".git" | while read file; do + clang-format-14 -i "$file" + done + + - name: Commit formatting changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + git diff --quiet && git diff --staged --quiet || git commit -m "style: apply clang-format" + git push diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 0000000..efbae4f --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,64 @@ +name: clang-tidy + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + +jobs: + clang-tidy: + name: clang-tidy + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang-tidy-14 lld-14 cmake ninja-build + + - name: Cache CMake build + uses: actions/cache@v3 + with: + path: | + ~/.cache/clang-tidy + build/ + key: ${{ runner.os }}-clang-tidy-${{ hashFiles('CMakeLists.txt', 'src/**', 'lib/**') }} + restore-keys: | + ${{ runner.os }}-clang-tidy- + + - name: Configure CMake with clang-tidy + run: | + mkdir -p build + cd build + + # Generate compile_commands.json with clang-tidy flags + cmake -G Ninja \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_CLANG_TIDY="clang-tidy-14;-checks=*,-modernize-use-trailing-return-type;-warnings-as-errors=*;-quiet" \ + .. + + - name: Build project + run: | + cd build + ninja -j$(nproc) + + - name: Run clang-tidy + run: | + cd build + # Run clang-tidy on all compilation units + ninja -t commands | xargs -I {} sh -c '{} 2>&1 | grep -v "^Checking" | grep -v "^Supppression"' || true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..03f1706 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: CodeQL + +on: + push: + branches: [ master, Matrix, Nexus ] + pull_request: + branches: [ master, Matrix, Nexus ] + schedule: + - cron: '0 0 * * 0' # Run every Sunday at midnight + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + config-file: .github/codeql/codeql-config.yml + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml new file mode 100644 index 0000000..c437b41 --- /dev/null +++ b/.github/workflows/cppcheck.yml @@ -0,0 +1,66 @@ +name: cppcheck + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'lib/**' + - 'src/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'lib/**' + - 'src/**' + +jobs: + cppcheck: + name: cppcheck + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache cppcheck + uses: actions/cache@v3 + with: + path: ~/.cache/cppcheck + key: ${{ runner.os }}-cppcheck-${{ hashFiles('src/**', 'lib/**') }} + restore-keys: | + ${{ runner.os }}-cppcheck- + + - name: Install cppcheck + run: | + sudo apt-get update + sudo apt-get install -y cppcheck + + - name: Run cppcheck + run: | + # Run cppcheck with comprehensive checks + cppcheck --enable=all \ + --inconclusive \ + --std=c++17 \ + --addon=misra \ + --library=posix \ + --verbose \ + --suppress=missingIncludeSystem \ + --suppress=unmatchedSuppression \ + --suppress=preprocessorErrorDirective \ + --suppress=*:lib/kissfft/* \ + --error-exitcode=1 \ + src/ lib/ + 2> cppcheck-report.txt || true + + - name: Upload cppcheck report + uses: actions/upload-artifact@v4 + if: always() + with: + name: cppcheck-report + path: cppcheck-report.txt + retention-days: 30 diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml new file mode 100644 index 0000000..f800388 --- /dev/null +++ b/.github/workflows/fuzzing.yml @@ -0,0 +1,158 @@ +name: Fuzzing with AFL++ + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - 'lib/kissfft/**' + - 'src/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - 'lib/kissfft/**' + - 'src/**' + workflow_dispatch: + inputs: + fuzz-time: + description: 'Fuzzing duration in seconds (default: 600 = 10 minutes)' + required: false + default: '600' + target: + description: 'Fuzzing target (kissfft or all)' + required: false + default: 'kissfft' + cores: + description: 'Number of CPU cores to use (default: 2)' + required: false + default: '2' + +jobs: + fuzz-kissfft: + name: Fuzz kissfft library + runs-on: ubuntu-latest + container: + image: ghcr.io/google/oss-fuzz-base:latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache AFL++ corpus + uses: actions/cache@v3 + with: + path: | + .afl-cov/ + findings/ + key: ${{ runner.os }}-afl-corpus-${{ hashFiles('lib/kissfft/**', 'src/**') }} + restore-keys: | + ${{ runner.os }}-afl-corpus- + + - name: Install dependencies + run: | + apt-get update && apt-get install -y cmake clang libgl1-mesa-dev + + - name: Build kissfft with fuzzing instrumentation + run: | + mkdir -p build-fuzz + cd build-fuzz + + # Build kissfft with AFL++ instrumentation + CC=afl-cc CXX=afl-c++ cmake .. -DCMAKE_BUILD_TYPE=Debug + make -j$(nproc) + + - name: Prepare fuzzing targets + run: | + # Create a simple fuzzing target for kissfft + cat > fuzz_target.cpp << 'EOF' + #include "../lib/kissfft/kiss_fft.h" + #include + #include + #include + #include + + extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < 2 * sizeof(float)) return 0; + + // Convert input to float samples + const float *samples = reinterpret_cast(data); + int nfft = size / sizeof(float); + + // Ensure nfft is a power of 2 for FFT (max 2^16 for performance) + int power_of_2 = 1; + while (power_of_2 * 2 <= nfft && power_of_2 <= 65536) power_of_2 *= 2; + nfft = std::min(power_of_2, 65536); + + // Allocate buffers + kiss_fft_cpx *in = new kiss_fft_cpx[nfft/2 + 1]; + kiss_fft_cpx *out = new kiss_fft_cpx[nfft/2 + 1]; + kiss_fftr_cfg cfg = kiss_fftr_alloc(nfft, 0, 0, 0); + + if (!in || !out || !cfg) { + delete[] in; + delete[] out; + if (cfg) kiss_fftr_free(cfg); + return 0; + } + + // Fill input with fuzzed data (ensure finite values) + for (int i = 0; i < nfft/2 + 1; i++) { + float r = samples[i*2]; + float im = samples[i*2 + 1]; + in[i].r = std::isfinite(r) ? r : 0.0f; + in[i].i = std::isfinite(im) ? im : 0.0f; + } + + // Perform FFT + kiss_fftr(cfg, in, out); + + // Cleanup + delete[] in; + delete[] out; + kiss_fftr_free(cfg); + + return 0; + } + EOF + + # Compile fuzzing target + afl-c++ -o fuzz_target fuzz_target.cpp -I../lib/kissfft build-fuzz/libkissfft.a -lm + + - name: Run AFL++ fuzzer + run: | + mkdir -p findings + FUZZ_TIME=${{ github.event.inputs.fuzz-time || '600' }} + CORES=${{ github.event.inputs.cores || '2' }} + + # Run AFL++ with parallel fuzzing if multiple cores + if [ "$CORES" -gt 1 ]; then + # Split into multiple instances + for i in $(seq 1 $CORES); do + mkdir -p findings_$i + timeout $FUZZ_TIME afl-fuzz -i /dev/null -o findings_$i -t 5000 -- ./fuzz_target -M main_$i & + done + wait + # Merge findings + mkdir -p findings + for i in $(seq 1 $CORES); do + cp findings_$i/queue/* findings/ 2>/dev/null || true + cp findings_$i/crashes/* findings/ 2>/dev/null || true + done + else + timeout $FUZZ_TIME afl-fuzz -i /dev/null -o findings -t 5000 -- ./fuzz_target || true + fi + + - name: Upload fuzzing findings + uses: actions/upload-artifact@v4 + if: always() + with: + name: fuzzing-findings + path: findings/ + retention-days: 30 + + - name: Upload fuzzing corpus + uses: actions/upload-artifact@v4 + if: always() + with: + name: fuzzing-corpus + path: findings/queue/ + retention-days: 7 diff --git a/.github/workflows/kodi-addon-checks.yml b/.github/workflows/kodi-addon-checks.yml new file mode 100644 index 0000000..d6f532d --- /dev/null +++ b/.github/workflows/kodi-addon-checks.yml @@ -0,0 +1,182 @@ +name: Kodi Addon Specific Checks + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - 'addon.xml' + - 'addon.xml.in' + - 'visualization.matrix/**' + - 'src/**' + - 'lib/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - 'addon.xml' + - 'addon.xml.in' + - 'visualization.matrix/**' + - 'src/**' + - 'lib/**' + +jobs: + validate-addon-xml: + name: Validate Addon XML + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate addon.xml structure + run: | + # Check if addon.xml exists or needs to be generated + if [ -f "addon.xml" ]; then + echo "Validating existing addon.xml" + xmllint --noout --schema https://raw.githubusercontent.com/xbmc/xbmc/master/xbmc/addons/dtd/addon.dtd addon.xml || true + elif [ -f "addon.xml.in" ]; then + echo "addon.xml.in found - this will be processed by CMake" + # Check for required fields in addon.xml.in + grep -q "id=\"visualization.matrix\"" addon.xml.in || { echo "Missing addon id"; exit 1; } + grep -q "version=" addon.xml.in || { echo "Missing version"; exit 1; } + grep -q "name=" addon.xml.in || { echo "Missing name"; exit 1; } + grep -q "provider=" addon.xml.in || { echo "Missing provider"; exit 1; } + else + echo "No addon.xml or addon.xml.in found" + exit 1 + fi + + - name: Check addon version format + run: | + if [ -f "addon.xml" ]; then + VERSION=$(grep -oP 'version="\K[^"]+' addon.xml) + elif [ -f "addon.xml.in" ]; then + VERSION=$(grep -oP 'version="\K[^"]+' addon.xml.in) + fi + + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Version $VERSION does not match semantic versioning (X.Y.Z)" + exit 1 + fi + echo "Version $VERSION is valid" + + check-kodi-compatibility: + name: Check Kodi Compatibility + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check Kodi API usage + run: | + # Check for deprecated Kodi API calls + DEPRECATED_APIS=( + "xbmc->Log" # Use CLog instead + "xbmc->Output" # Use CLog instead + "ADDON::CAddon" # Use kodi::addon::CAddon + ) + + for api in "${DEPRECATED_APIS[@]}"; do + if grep -r "$api" src/ lib/ visualization.matrix/ 2>/dev/null; then + echo "⚠️ Deprecated Kodi API found: $api" + echo "Please update to use the current Kodi API" + fi + done + + - name: Check for required Kodi includes + run: | + REQUIRED_INCLUDES=( + "kodi/addon-instance/Visualization.h" + "kodi/General.h" + "kodi/gui/gl/GL.h" + ) + + for include in "${REQUIRED_INCLUDES[@]}"; do + if ! grep -r "#include.*$include" src/ lib/ visualization.matrix/ 2>/dev/null; then + echo "⚠️ Recommended Kodi include not found: $include" + fi + done + + check-visualization-specifics: + name: Check Visualization-Specific Requirements + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for required visualization methods + run: | + REQUIRED_METHODS=( + "Create" + "Start" + "Stop" + "Render" + "AudioData" + ) + + for method in "${REQUIRED_METHODS[@]}"; do + if ! grep -r "$method" src/ visualization.matrix/ 2>/dev/null; then + echo "⚠️ Required visualization method not found: $method" + fi + done + + - name: Check for OpenGL/ES compatibility + run: | + # Check for OpenGL usage + if ! grep -r "gl\|GL_\|GLuint\|GLfloat" src/ lib/ visualization.matrix/ 2>/dev/null; then + echo "⚠️ No OpenGL usage detected - is this a visualization addon?" + fi + + # Check for GLES compatibility + if grep -r "GLES\|OPENGLES" src/ lib/ visualization.matrix/ 2>/dev/null; then + echo "✅ OpenGL ES compatibility detected" + fi + + - name: Check for thread safety + run: | + # Check for potential thread safety issues + if grep -r "new \|malloc\|free\|delete" src/ lib/ visualization.matrix/ 2>/dev/null; then + echo "⚠️ Manual memory management found - consider using smart pointers" + fi + + # Check for mutex usage + if ! grep -r "std::mutex\|std::lock_guard\|std::unique_lock" src/ lib/ visualization.matrix/ 2>/dev/null; then + echo "ℹ️ No mutex usage detected - ensure thread safety if using multiple threads" + fi + + check-performance: + name: Check Performance Considerations + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for expensive operations in render loop + run: | + # Check for potentially expensive operations + EXPENSIVE_OPS=( + "new \[\|malloc" + "std::vector::resize" + "memcpy\|memset" + "printf\|sprintf" + ) + + for op in "${EXPENSIVE_OPS[@]}"; do + if grep -r "$op" src/ visualization.matrix/ 2>/dev/null; then + echo "⚠️ Potentially expensive operation in render path: $op" + fi + done + + - name: Check for FFT optimization + run: | + # Check kissfft usage + if grep -r "kiss_fft\|kiss_fftr" src/ lib/ 2>/dev/null; then + echo "✅ FFT library (kissfft) detected" + + # Check for FFT size validation + if ! grep -r "power.*2\|nfft.*2" src/ 2>/dev/null; then + echo "⚠️ Consider validating FFT size is a power of 2" + fi + fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..df8db92 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,293 @@ +name: Main CI/CD Pipeline + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + - 'lib/**' + - 'src/**' + - 'Dockerfile' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + - 'lib/**' + - 'src/**' + - 'Dockerfile' + workflow_dispatch: + inputs: + run-tests: + description: 'Run tests' + required: false + default: 'true' + run-security: + description: 'Run security scans' + required: false + default: 'true' + run-build: + description: 'Run builds' + required: false + default: 'true' + +jobs: + # Static Analysis Jobs + static-analysis: + name: Static Analysis + runs-on: ubuntu-latest + strategy: + matrix: + tool: [codeql, clang-tidy, cppcheck] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for CodeQL + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cache/clang-tidy + ~/.cache/cppcheck + ~/.cache/CMake + key: ${{ runner.os }}-${{ matrix.tool }}-${{ hashFiles('CMakeLists.txt', 'src/**', 'lib/**') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.tool }}- + + - name: Install dependencies + if: matrix.tool != 'codeql' + run: | + sudo apt-get update + sudo apt-get install -y clang-tidy-14 cmake ninja-build cppcheck + + - name: Run CodeQL + if: matrix.tool == 'codeql' + uses: github/codeql-action/analyze@v3 + with: + languages: cpp + queries: security-and-quality + + - name: Run clang-tidy + if: matrix.tool == 'clang-tidy' + run: | + mkdir -p build && cd build + cmake -G Ninja -DCMAKE_CXX_CLANG_TIDY="clang-tidy-14;-checks=*;-warnings-as-errors=*" .. + ninja -j$(nproc) + + - name: Run cppcheck + if: matrix.tool == 'cppcheck' + run: | + cppcheck --enable=all --inconclusive --std=c++17 src/ lib/ --error-exitcode=1 + + # Security Scan Jobs + security-scan: + name: Security Scan + runs-on: ubuntu-latest + strategy: + matrix: + tool: [trivy, snyk] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache security tools + uses: actions/cache@v3 + with: + path: | + ~/.cache/trivy + ~/.cache/snyk + key: ${{ runner.os }}-security-${{ matrix.tool }}-${{ hashFiles('.') }} + restore-keys: | + ${{ runner.os }}-security-${{ matrix.tool }}- + + - name: Run Trivy + if: matrix.tool == 'trivy' + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH,MEDIUM' + + - name: Upload Trivy SARIF + if: matrix.tool == 'trivy' + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + - name: Run Snyk + if: matrix.tool == 'snyk' + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --all-projects --severity-threshold=high + + # Build Jobs + build: + name: Build + needs: [static-analysis, security-scan] + runs-on: ubuntu-latest + strategy: + matrix: + config: + - name: "Linux (Debug)" + build_type: Debug + cmake_flags: "" + - name: "Linux (Release)" + build_type: Release + cmake_flags: "-DCMAKE_BUILD_TYPE=Release" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache CMake + uses: actions/cache@v3 + with: + path: | + ~/.cache/CMake + build/ + key: ${{ runner.os }}-cmake-${{ matrix.config.build_type }}-${{ hashFiles('CMakeLists.txt', 'src/**', 'lib/**') }} + restore-keys: | + ${{ runner.os }}-cmake-${{ matrix.config.build_type }}- + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake g++ make libgl1-mesa-dev libglu1-mesa-dev ninja-build + + - name: Configure CMake + run: | + mkdir -p build && cd build + cmake -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} .. + + - name: Build project + run: | + cd build + ninja -j$(nproc) + + - name: Run tests (if available) + run: | + cd build + ctest --output-on-failure || echo "No tests found" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-${{ matrix.config.name }} + path: build/ + retention-days: 7 + + # Docker Build + docker-build: + name: Docker Build + runs-on: ubuntu-latest + needs: [build] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-docker-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-docker- + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build Docker image + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: false + tags: visualization.matrix:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + - name: Test Docker image + run: | + docker run --rm visualization.matrix:latest echo "Docker build test successful" + + # SBOM Generation + sbom: + name: SBOM Generation + runs-on: ubuntu-latest + needs: [build] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Syft + uses: actions/cache@v3 + with: + path: ~/.cache/syft + key: ${{ runner.os }}-syft-${{ hashFiles('.') }} + restore-keys: | + ${{ runner.os }}-syft- + + - name: Install Syft + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Generate SBOM + run: | + syft dir:. -o spdx-json --file sbom.spdx.json + syft dir:. -o cyclonedx-json --file sbom.cyclonedx.json + + - name: Upload SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom + path: | + sbom.spdx.json + sbom.cyclonedx.json + retention-days: 30 + + # Performance Benchmark (optional) + benchmark: + name: Performance Benchmark + runs-on: ubuntu-latest + needs: [build] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-Linux (Release) + path: build + + - name: Run performance tests + run: | + cd build + # Placeholder for actual benchmark commands + echo "Performance benchmarking would run here" diff --git a/.github/workflows/owasp-zap.yml b/.github/workflows/owasp-zap.yml new file mode 100644 index 0000000..1ce132c --- /dev/null +++ b/.github/workflows/owasp-zap.yml @@ -0,0 +1,103 @@ +name: OWASP ZAP Dynamic Analysis + +on: + workflow_dispatch: + inputs: + target-url: + description: 'URL to scan (default: localhost:8080)' + required: false + default: 'http://localhost:8080' + scan-type: + description: 'Scan type (baseline, full, or api)' + required: false + default: 'baseline' + auto-approve: + description: 'Auto-approve alerts (for testing only)' + required: false + default: 'false' + schedule: + - cron: '0 0 * * 0' # Run every Sunday at midnight + +jobs: + zap-scan: + name: OWASP ZAP Scan + runs-on: ubuntu-latest + container: + image: owasp/zap2docker-weekly:latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Start ZAP in daemon mode + run: | + zap-baseline.py -t ${{ github.event.inputs.target-url }} -r zap-report.html || true + + - name: Upload ZAP report + uses: actions/upload-artifact@v4 + if: always() + with: + name: zap-report + path: zap-report.html + retention-days: 30 + + - name: Convert report to SARIF + if: always() + run: | + # Install jq for JSON processing + apt-get update && apt-get install -y jq + + # Convert HTML report to SARIF (simplified) + cat > zap-to-sarif.py << 'EOF' + import json + import re + + # Read ZAP HTML report and extract alerts + with open('zap-report.html', 'r') as f: + content = f.read() + + # Simple SARIF structure + sarif = { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [{ + "tool": { + "driver": { + "name": "OWASP ZAP", + "informationUri": "https://www.zaproxy.org/", + "version": "2.10.0" + } + }, + "results": [] + }] + } + + # Extract alerts from HTML (simplified regex) + alert_pattern = r'.*?(.*?).*?(.*?).*?(.*?).*?(.*?)' + alerts = re.findall(alert_pattern, content, re.DOTALL) + + for alert in alerts: + if len(alert) >= 4: + risk, name, url, _ = alert + sarif["runs"][0]["results"].append({ + "ruleId": name.strip(), + "level": risk.strip().lower(), + "message": {"text": f"{name.strip()} found at {url.strip()}"}, + "locations": [{ + "physicalLocation": { + "artifactLocation": {"uri": url.strip()} + } + }] + }) + + with open('zap-report.sarif', 'w') as f: + json.dump(sarif, f, indent=2) + EOF + + python3 zap-to-sarif.py || true + + - name: Upload SARIF report + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'zap-report.sarif' diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..882f87e --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,55 @@ +name: SBOM Generation + +on: + push: + branches: [ master, Matrix, Nexus ] + tags: [ 'v*' ] + pull_request: + branches: [ master, Matrix, Nexus ] + release: + types: [ published ] + +jobs: + generate-sbom: + name: Generate SBOM with Syft + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Syft + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Generate SBOM + run: | + syft dir:. -o spdx-json --file sbom.spdx.json + syft dir:. -o cyclonedx-json --file sbom.cyclonedx.json + + - name: Upload SBOM artifacts + uses: actions/upload-artifact@v4 + with: + name: sbom + path: | + sbom.spdx.json + sbom.cyclonedx.json + retention-days: 90 + + - name: Upload SBOM to GitHub Release (if tag) + if: github.event_name == 'release' + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ github.event.release.upload_url }} + asset_name: sbom.spdx.json + asset_path: sbom.spdx.json + asset_content_type: application/json + + - name: Upload SBOM to GitHub Release (CycloneDX) + if: github.event_name == 'release' + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ github.event.release.upload_url }} + asset_name: sbom.cyclonedx.json + asset_path: sbom.cyclonedx.json + asset_content_type: application/json diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..d6566af --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,107 @@ +name: Semgrep Static Analysis + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'lib/**' + - 'src/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'lib/**' + - 'src/**' + schedule: + - cron: '0 0 * * 0' # Run every Sunday at midnight + +jobs: + semgrep-scan: + name: Semgrep Scan + runs-on: ubuntu-latest + container: + image: returntocorp/semgrep:latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Semgrep + uses: actions/cache@v3 + with: + path: ~/.cache/semgrep + key: ${{ runner.os }}-semgrep-${{ hashFiles('.semgrep.yml', 'src/**', 'lib/**') }} + restore-keys: | + ${{ runner.os }}-semgrep- + + - name: Run Semgrep with custom rules + run: | + # Run Semgrep with default rules + custom rules + semgrep scan \ + --config=auto \ + --config=.semgrep.yml \ + --error \ + --json \ + --output=semgrep-results.json \ + src/ lib/ || true + + - name: Upload Semgrep report + uses: actions/upload-artifact@v4 + if: always() + with: + name: semgrep-report + path: semgrep-results.json + retention-days: 30 + + - name: Convert Semgrep JSON to SARIF + if: always() + run: | + # Install jq for JSON processing + apt-get update && apt-get install -y jq + + # Convert Semgrep JSON to SARIF format + jq -r ' + { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [{ + "tool": { + "driver": { + "name": "Semgrep", + "informationUri": "https://semgrep.dev/", + "version": "latest", + "rules": [] + } + }, + "results": [ + .[] | { + "ruleId": .check_id, + "level": (if .severity == "ERROR" then "error" elif .severity == "WARNING" then "warning" else "note" end), + "message": {"text": .extra.message}, + "locations": [{ + "physicalLocation": { + "artifactLocation": {"uri": .path}, + "region": { + "startLine": .start.line, + "startColumn": .start.col, + "endLine": .end.line, + "endColumn": .end.col + } + } + }] + } + ] + }] + } + ' semgrep-results.json > semgrep-results.sarif || true + + - name: Upload SARIF report to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'semgrep-results.sarif' diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 0000000..6ecb66c --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,87 @@ +name: Snyk Security Scan + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + - 'lib/**' + - 'src/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + - 'lib/**' + - 'src/**' + schedule: + - cron: '0 0 * * 1' # Run every Monday at midnight + +jobs: + snyk-scan: + name: Snyk Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Snyk + uses: actions/cache@v3 + with: + path: ~/.cache/snyk + key: ${{ runner.os }}-snyk-${{ hashFiles('.') }} + restore-keys: | + ${{ runner.os }}-snyk- + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --all-projects --severity-threshold=high + + - name: Upload Snyk report + uses: actions/upload-artifact@v4 + if: always() + with: + name: snyk-report + path: .snyk + retention-days: 30 + + snyk-code: + name: Snyk Code Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Snyk Code + uses: actions/cache@v3 + with: + path: ~/.cache/snyk-code + key: ${{ runner.os }}-snyk-code-${{ hashFiles('src/**', 'lib/**') }} + restore-keys: | + ${{ runner.os }}-snyk-code- + + - name: Run Snyk Code + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: code + args: test --severity-threshold=high + + - name: Upload Snyk Code report + uses: actions/upload-artifact@v4 + if: always() + with: + name: snyk-code-report + path: .snyk-code + retention-days: 30 diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 0000000..04a4907 --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,88 @@ +name: SonarQube Analysis + +on: + push: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + - 'lib/**' + - 'src/**' + pull_request: + branches: [ master, Matrix, Nexus ] + paths: + - '**.cpp' + - '**.h' + - '**.c' + - 'CMakeLists.txt' + - 'lib/**' + - 'src/**' + +jobs: + sonarqube: + name: SonarQube + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for SonarQube to detect PR base branch + + - name: Cache SonarQube + uses: actions/cache@v3 + with: + path: ~/.sonar + key: ${{ runner.os }}-sonar-${{ hashFiles('sonar-project.properties') }} + restore-keys: | + ${{ runner.os }}-sonar- + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake g++ make libgl1-mesa-dev + + - name: Configure CMake with coverage + run: | + mkdir -p build + cd build + cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="--coverage" .. + + - name: Build project + run: | + cd build + make -j$(nproc) + + - name: Run tests with coverage + run: | + cd build + # Run tests if available, otherwise skip + ctest --output-on-failure || echo "No tests found" + # Generate coverage report + gcov -r . 2>/dev/null || true + lcov --capture --directory . --output-file coverage.info 2>/dev/null || true + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + with: + projectBaseDir: . + args: | + -Dsonar.projectKey=visualization.matrix + -Dsonar.projectName=visualization.matrix + -Dsonar.sources=src,lib + -Dsonar.cxx.coverage.reportPaths=build/coverage.info + -Dsonar.cxx.clang-tidy.report-path=build/clang-tidy-report.txt + -Dsonar.cxx.cppcheck.report-path=build/cppcheck-report.txt + + - name: Upload SonarQube report + uses: actions/upload-artifact@v4 + if: always() + with: + name: sonarqube-report + path: .scannerwork/ + retention-days: 30 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..8394d97 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,33 @@ +name: Trivy Vulnerability Scan + +on: + push: + branches: [ master, Matrix, Nexus ] + pull_request: + branches: [ master, Matrix, Nexus ] + schedule: + - cron: '0 0 * * 1' # Run every Monday at midnight + +jobs: + trivy-scan: + name: Trivy Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/.semgrep.yml b/.semgrep.yml new file mode 100644 index 0000000..e87112e --- /dev/null +++ b/.semgrep.yml @@ -0,0 +1,261 @@ +# Semgrep Configuration for visualization.matrix +# Custom security rules for C++ code analysis +# Documentation: https://semgrep.dev/docs/writing-rules/rule-ideas/ + +rules: + # ============================================ + # Memory Safety Rules + # ============================================ + + - id: unsafe-malloc + pattern: malloc($SIZE) + message: "Avoid using malloc(). Use new or smart pointers (std::unique_ptr, std::shared_ptr) instead." + languages: [cpp] + severity: ERROR + metadata: + cwe: "CWE-759: Use of Unsafe Function" + category: memory-safety + + - id: unsafe-free + pattern: free($PTR) + message: "Avoid using free(). Use delete or smart pointers instead." + languages: [cpp] + severity: ERROR + metadata: + cwe: "CWE-759: Use of Unsafe Function" + category: memory-safety + + - id: unsafe-strcpy + pattern: strcpy($DST, $SRC) + message: "Avoid using strcpy(). Use strncpy() or std::string instead." + languages: [cpp] + severity: ERROR + metadata: + cwe: "CWE-120: Buffer Copy without Checking Size of Input" + category: memory-safety + + - id: unsafe-strcat + pattern: strcat($DST, $SRC) + message: "Avoid using strcat(). Use strncat() or std::string instead." + languages: [cpp] + severity: ERROR + metadata: + cwe: "CWE-120: Buffer Copy without Checking Size of Input" + category: memory-safety + + - id: unsafe-sprintf + pattern: sprintf($BUF, $FMT, ...) + message: "Avoid using sprintf(). Use snprintf() instead." + languages: [cpp] + severity: ERROR + metadata: + cwe: "CWE-134: Use of Externally-Controlled Format String" + category: memory-safety + + - id: unsafe-gets + pattern: gets($BUF) + message: "Never use gets(). It is inherently unsafe. Use fgets() instead." + languages: [cpp] + severity: ERROR + metadata: + cwe: "CWE-242: Use of Unchecked Externally Provided Data" + category: memory-safety + + # ============================================ + # Buffer Overflow Prevention + # ============================================ + + - id: unbounded-array-access + pattern: $ARR[$INDEX] + message: "Potential unbounded array access. Ensure INDEX is within bounds." + languages: [cpp] + severity: WARNING + metadata: + cwe: "CWE-125: Out-of-bounds Read" + category: memory-safety + fix: | + if ($INDEX >= 0 && $INDEX < sizeof($ARR)/sizeof($ARR[0])) { $ARR[$INDEX] } + + - id: unsafe-pointer-arithmetic + pattern: $PTR + $OFFSET + message: "Pointer arithmetic can lead to buffer overflows. Validate offset bounds." + languages: [cpp] + severity: WARNING + metadata: + cwe: "CWE-466: Return of Pointer Value Outside of Expected Range" + category: memory-safety + + # ============================================ + # Kodi-Specific Security Rules + # ============================================ + + - id: kodi-deprecated-log + pattern: xbmc->Log($LEVEL, $MSG) + message: "xbmc->Log is deprecated. Use CLog::Log() instead." + languages: [cpp] + severity: WARNING + metadata: + category: kodi-specific + fix: CLog::Log($LEVEL, $MSG) + + - id: kodi-deprecated-output + pattern: xbmc->Output($LEVEL, $MSG) + message: "xbmc->Output is deprecated. Use CLog::Log() instead." + languages: [cpp] + severity: WARNING + metadata: + category: kodi-specific + fix: CLog::Log($LEVEL, $MSG) + + - id: kodi-addon-missing-create + pattern: class $CLASS : public $KODI_BASE + message: "Kodi visualization addons must implement the Create() method." + languages: [cpp] + severity: ERROR + metadata: + category: kodi-specific + fix: | + class $CLASS : public $KODI_BASE { + public: + bool Create() override { /* implementation */ } + } + + - id: kodi-addon-missing-start + pattern: class $CLASS : public $KODI_BASE + message: "Kodi visualization addons must implement the Start() method." + languages: [cpp] + severity: ERROR + metadata: + category: kodi-specific + + - id: kodi-addon-missing-stop + pattern: class $CLASS : public $KODI_BASE + message: "Kodi visualization addons must implement the Stop() method." + languages: [cpp] + severity: ERROR + metadata: + category: kodi-specific + + - id: kodi-addon-missing-render + pattern: class $CLASS : public $KODI_BASE + message: "Kodi visualization addons must implement the Render() method." + languages: [cpp] + severity: ERROR + metadata: + category: kodi-specific + + # ============================================ + # FFT-Specific Rules (for kissfft) + # ============================================ + + - id: fft-input-validation + pattern: kiss_fftr($CFG, $IN, $OUT) + message: "Ensure FFT input is validated for NaN/Inf values to prevent undefined behavior." + languages: [cpp] + severity: WARNING + metadata: + category: fft-specific + fix: | + for (int i = 0; i < nfft; i++) { + if (!std::isfinite($IN[i].r)) $IN[i].r = 0.0f; + if (!std::isfinite($IN[i].i)) $IN[i].i = 0.0f; + } + kiss_fftr($CFG, $IN, $OUT) + + - id: fft-size-power-of-two + pattern: kiss_fftr_alloc($NFFT, ...) + message: "FFT size should be a power of two for optimal performance." + languages: [cpp] + severity: INFO + metadata: + category: fft-specific + + # ============================================ + # Error Handling + # ============================================ + + - id: missing-null-check + pattern: $PTR->$METHOD(...) + message: "Potential null pointer dereference. Add null check before accessing member." + languages: [cpp] + severity: WARNING + metadata: + cwe: "CWE-476: NULL Pointer Dereference" + category: error-handling + fix: if ($PTR) { $PTR->$METHOD(...) } + + - id: missing-exception-handling + pattern: throw $EXCEPTION + message: "Consider adding exception handling for robust error recovery." + languages: [cpp] + severity: INFO + metadata: + category: error-handling + + # ============================================ + # Performance Rules + # ============================================ + + - id: expensive-operation-in-loop + pattern: | + for (...) { + $EXPENSIVE_OP(...) + } + message: "Expensive operation in loop. Consider optimizing or caching." + languages: [cpp] + severity: INFO + metadata: + category: performance + patterns: + - pattern-inside: for (...) + - pattern: new $TYPE + - pattern: malloc(...) + - pattern: memcpy(...) + + - id: use-emplace-back + pattern: $VECTOR.push_back($OBJ) + message: "Use emplace_back() instead of push_back() for better performance with complex objects." + languages: [cpp] + severity: INFO + metadata: + category: performance + fix: $VECTOR.emplace_back(...) + + # ============================================ + # Modern C++ Best Practices + # ============================================ + + - id: use-const-reference + pattern: $TYPE $PARAM + message: "Consider passing by const reference to avoid unnecessary copies." + languages: [cpp] + severity: INFO + metadata: + category: modern-cpp + fix: const $TYPE& $PARAM + + - id: use-nullptr + pattern: NULL + message: "Use nullptr instead of NULL for modern C++." + languages: [cpp] + severity: INFO + metadata: + category: modern-cpp + fix: nullptr + + - id: use-auto + pattern: $TYPE $VAR = ... + message: "Consider using auto for type deduction where appropriate." + languages: [cpp] + severity: INFO + metadata: + category: modern-cpp + + - id: use-range-based-for + pattern: for (int i = 0; i < $SIZE; i++) + message: "Consider using range-based for loop for better readability." + languages: [cpp] + severity: INFO + metadata: + category: modern-cpp + fix: for (auto& item : $CONTAINER) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3ffa845 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,85 @@ +# Changelog + +All notable changes to the **visualization.matrix** addon will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## 📌 Unreleased + +### ⚠️ Security +- Added comprehensive security framework with 11 analysis tools +- Added `SECURITY.md` with vulnerability reporting guidelines +- Added `DEPENDENCY_POLICY.md` for dependency management + +### ✨ Features +- Added static analysis with CodeQL, clang-tidy, cppcheck, and SonarQube +- Added dynamic analysis with OWASP ZAP +- Added vulnerability scanning with Trivy and Snyk +- Added fuzzing with AFL++ for kissfft library +- Added SBOM generation with Syft (SPDX and CycloneDX) +- Added Kodi-specific validation checks + +### 📝 Documentation +- Added `CONTRIBUTING.md` with contribution guidelines +- Added `CODE_OF_CONDUCT.md` based on Contributor Covenant v1.4 +- Added issue templates for bug reports, feature requests, and security vulnerabilities +- Added pull request template with security checklist +- Updated `README.md` with security information + +### 🔧 CI/CD +- Added 13 GitHub Actions workflows for comprehensive CI/CD +- Added caching for all workflows to improve performance +- Added path filtering to reduce CI/CD costs +- Added CODEOWNERS and labeler for repository management + +### 🐛 Bug Fixes +- None in this release + +### 🧹 Maintenance +- None in this release + +--- + +## 📅 Release History + +### [1.0.0] - 2025-07-15 +#### ✨ Initial Release +- First stable release of visualization.matrix addon +- Basic visualization functionality for Kodi +- Support for OpenGL rendering + +--- + +## 📊 Types of Changes + +- **Added**: for new features. +- **Changed**: for changes in existing functionality. +- **Deprecated**: for soon-to-be removed features. +- **Removed**: for now removed features. +- **Fixed**: for any bug fixes. +- **Security**: in case of vulnerabilities. + +--- + +## 🔖 Versioning + +This project uses **Semantic Versioning** (`MAJOR.MINOR.PATCH`): + +- **MAJOR**: Breaking changes, incompatible API modifications +- **MINOR**: Backwards-compatible new features +- **PATCH**: Backwards-compatible bug fixes + +--- + +## 📝 How to Contribute + +See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. + +--- + +## 🛡️ Security + +See [SECURITY.md](SECURITY.md) for vulnerability reporting. diff --git a/CMakeLists.txt b/CMakeLists.txt index 231fa68..6a1be4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,43 @@ -cmake_minimum_required(VERSION 3.5) -project(visualization.matrix) +cmake_minimum_required(VERSION 3.20) +project(visualization.matrix VERSION 1.0.0 LANGUAGES CXX) +# Project metadata +set(PROJECT_DESCRIPTION "Matrix themed visualization addon for Kodi") +set(PROJECT_AUTHOR "Team Kodi") +set(PROJECT_LICENSE "GPL-2.0-or-later") + +# Set C++ standard (C++17 for Kodi Matrix compatibility) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Enable position independent code +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Set CMake module path set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}) +# Options +option(BUILD_TESTING "Build tests" OFF) +option(ENABLE_COVERAGE "Enable code coverage" OFF) +option(ENABLE_CLANG_TIDY "Enable clang-tidy" OFF) +option(ENABLE_CPPCHECK "Enable cppcheck" OFF) + +# Fetch kissfft dependency +include(FetchContent) +FetchContent_Declare( + kissfft + GIT_REPOSITORY https://github.com/mborgerding/kissfft.git + GIT_TAG master + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(kissfft) + +# Find required packages find_package(Kodi REQUIRED) find_package(glm REQUIRED) -add_subdirectory(lib/kissfft) - +# Platform-specific OpenGL handling if(NOT WIN32 AND (APP_RENDER_SYSTEM STREQUAL "gl" OR NOT APP_RENDER_SYSTEM)) find_package(OpenGl REQUIRED) set(DEPLIBS ${OPENGL_LIBRARIES}) @@ -20,22 +50,91 @@ else() add_definitions(${OPENGLES_DEFINITIONS}) endif() -include_directories(${GLM_INCLUDE_DIR} - ${KODI_INCLUDE_DIR}/.. # Hack way with "/..", need bigger Kodi cmake rework to match right include ways - ${PROJECT_SOURCE_DIR}/lib) +# Include directories +include_directories( + ${GLM_INCLUDE_DIR} + ${KODI_INCLUDE_DIR}/.. + ${PROJECT_SOURCE_DIR}/lib + ${kissfft_SOURCE_DIR} +) +# Platform-specific libraries if(CORE_SYSTEM_NAME STREQUAL osx OR CORE_SYSTEM_NAME STREQUAL ios OR CORE_SYSTEM_NAME STREQUAL darwin_embedded) list(APPEND DEPLIBS "-framework CoreVideo") endif() -set(MATRIX_SOURCES src/main.cpp) +# Source files +set(MATRIX_SOURCES + src/main.cpp + ${kissfft_SOURCE_DIR}/kiss_fft.c +) + +set(MATRIX_HEADERS + src/main.h + src/stb_image.h + ${kissfft_SOURCE_DIR}/kiss_fft.h + ${kissfft_SOURCE_DIR}/kiss_fft_log.h + ${kissfft_SOURCE_DIR}/_kiss_fft_guts.h +) + +# Create kissfft library target +add_library(kissfft STATIC + ${kissfft_SOURCE_DIR}/kiss_fft.c + ${kissfft_SOURCE_DIR}/kiss_fft.h + ${kissfft_SOURCE_DIR}/kiss_fft_log.h + ${kissfft_SOURCE_DIR}/_kiss_fft_guts.h +) +target_include_directories(kissfft PUBLIC ${kissfft_SOURCE_DIR}) -set(MATRIX_HEADERS src/main.h) +# Main visualization addon +add_library(visualization.matrix MODULE + ${MATRIX_SOURCES} + ${MATRIX_HEADERS} +) -list(APPEND DEPLIBS kissfft) +# Link libraries +target_link_libraries(visualization.matrix PRIVATE + kissfft + ${DEPLIBS} + ${KODI_LIBRARIES} +) -build_addon(visualization.matrix MATRIX DEPLIBS) +# Set target properties +target_compile_options(visualization.matrix PRIVATE + $<$:-Wall -Wextra -Wpedantic> + $<$:-Wall -Wextra -Wpedantic> +) +# Install target +install(TARGETS visualization.matrix + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX} +) + +# CPack configuration include(CPack) +set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_PACKAGE_VENDOR ${PROJECT_AUTHOR}) +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_DESCRIPTION}) +set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE.md) + +# Enable testing if requested +if(BUILD_TESTING) + enable_testing() + # Add tests here when available +endif() + +# Code coverage +if(ENABLE_COVERAGE) + target_compile_options(visualization.matrix PRIVATE --coverage) + target_link_options(visualization.matrix PRIVATE --coverage) +endif() + +# Clang-tidy integration +if(ENABLE_CLANG_TIDY AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_target_properties(visualization.matrix PROPERTIES + CXX_CLANG_TIDY "clang-tidy;-checks=*;-warnings-as-errors=*" + ) +endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c42aec1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,55 @@ +# Code of Conduct for visualization.matrix + +## 🤝 Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +--- + +## 🌟 Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +--- + +## 🛡️ Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +--- + +## 🌍 Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +--- + +## 📜 Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainers at **`security@marcelraschke.de`**. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +--- + +## 📜 Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html). + +For answers to common questions about this code of conduct, see its [FAQ](https://www.contributor-covenant.org/faq). Translations are available [here](https://www.contributor-covenant.org/translations). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..902a952 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,198 @@ +# Contributing to visualization.matrix + +Thank you for your interest in contributing to **visualization.matrix**! This document provides guidelines to ensure a smooth and secure contribution process. + +--- + +## 📜 Code of Conduct + +By participating in this project, you agree to abide by the [Kodi Code of Conduct](https://kodi.tv/about/code-of-conduct/). Be respectful, inclusive, and collaborative. + +--- + +## 🚀 How to Contribute + +### Reporting Bugs + +1. **Check existing issues**: Search the [GitHub Issues](https://github.com/MarcelRaschke/visualization.matrix/issues) to ensure the bug hasn't already been reported. +2. **Create a new issue**: If the bug is new, open an issue with: + - A **clear title** describing the problem. + - A **detailed description** of the issue, including steps to reproduce. + - **Logs or screenshots** (if applicable). + - Your **Kodi version** and **operating system**. + +### Suggesting Enhancements + +1. Open an issue with the **"enhancement"** label. +2. Describe the **feature request** in detail, including: + - The **use case** for the feature. + - Any **mockups or examples** (if applicable). + +### Submitting Pull Requests + +1. **Fork the repository** and create a feature branch (`git checkout -b feature/your-feature`). +2. **Commit your changes** with clear, descriptive messages (follow [Conventional Commits](https://www.conventionalcommits.org/) if possible). +3. **Test your changes**: Ensure the addon builds and works as expected. +4. **Run security checks**: All pull requests are automatically scanned with: + - [CodeQL](https://codeql.github.com/) (static analysis) + - [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) (code quality) + - [cppcheck](https://cppcheck.sourceforge.io/) (static analysis) + - [Trivy](https://github.com/aquasecurity/trivy) (vulnerability scanning) +5. **Submit the PR**: Open a pull request to the `master` or `Matrix` branch (depending on the target). +6. **Address feedback**: Respond to review comments and update your PR as needed. + +--- + +## 🔧 Development Setup + +### Prerequisites + +- **CMake** (≥ 3.5) +- **C++ Compiler** (GCC ≥ 7, Clang ≥ 8, or MSVC ≥ 2017) +- **Git** +- **Kodi source code** (for building the addon) + +### Building the Addon + +Follow the [build instructions in README.md](README.md#build-instructions-for-linux). + +--- + +## 🛡️ Security Guidelines + +### Reporting Security Vulnerabilities + +**Do not** report security vulnerabilities in public issues or pull requests. Instead, follow the instructions in **[SECURITY.md](SECURITY.md)**. + +### Secure Coding Practices + +1. **Avoid unsafe functions**: + - Use `strncpy` instead of `strcpy`. + - Use `snprintf` instead of `sprintf`. + - Use `memcpy_s` or bounds-checked alternatives where available. + +2. **Input validation**: + - Validate all user inputs and external data. + - Use safe parsing libraries (e.g., avoid manual string parsing). + +3. **Memory management**: + - Avoid memory leaks (use smart pointers where possible). + - Check for `nullptr` before dereferencing pointers. + - Use bounds checking for array access. + +4. **Error handling**: + - Handle errors gracefully (avoid crashes or undefined behavior). + - Use assertions for internal consistency checks. + +5. **No hardcoded secrets**: + - Never commit API keys, passwords, or tokens to the repository. + - Use environment variables or secure configuration files. + +### Security Tools + +This project uses the following tools to enforce security: + +| Tool | Purpose | When It Runs | +|------|---------|--------------| +| [CodeQL](https://codeql.github.com/) | Static analysis for C++ vulnerabilities | Push, Pull Request, Weekly | +| [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) | Code quality and style checks | Push, Pull Request | +| [cppcheck](https://cppcheck.sourceforge.io/) | Additional static analysis | Push, Pull Request | +| [Trivy](https://github.com/aquasecurity/trivy) | Vulnerability scanning | Push, Pull Request, Weekly | +| [Dependabot](https://docs.github.com/en/code-security/dependabot) | Dependency updates | Weekly | +| [Syft](https://github.com/anchore/syft) | SBOM generation | Push, Pull Request, Release | +| [AFL++](https://github.com/AFLplusplus/AFLplusplus) | Fuzzing for `kissfft` | Push, Pull Request, Manual | + +--- + +## 📝 Coding Standards + +### C++ Style + +- Follow **modern C++** practices (C++17 or later). +- Use **RAII** (Resource Acquisition Is Initialization) for resource management. +- Prefer **`const` correctness** (mark variables and methods `const` where possible). +- Use **meaningful names** for variables, functions, and classes. +- Avoid **global variables** (use singletons or dependency injection if needed). + +### Formatting + +- **Indentation**: 4 spaces (no tabs). +- **Braces**: K&R style (opening brace on the same line). +- **Line length**: ≤ 120 characters. +- **Comments**: Use `//` for single-line comments and `/* */` for multi-line. + +Example: +```cpp +// Good +if (condition) { + doSomething(); +} + +// Bad +if(condition) +{ + doSomething(); +} +``` + +### Commit Messages + +- Use **imperative mood** (e.g., "Fix bug" instead of "Fixed bug"). +- Keep the **subject line ≤ 50 characters**. +- Provide a **detailed body** if the change is complex. +- Reference **issues or PRs** (e.g., "Closes #123"). + +Example: +``` +Fix buffer overflow in FFT processing + +- Replace unsafe memcpy with bounds-checked alternative +- Add input validation for sample size +- Closes #456 +``` + +--- + +## 🧪 Testing + +### Manual Testing + +1. Build the addon and install it in Kodi. +2. Test the visualization with various audio sources. +3. Verify that the addon does not crash or produce artifacts. + +### Automated Testing + +- **Static analysis**: All PRs are automatically scanned with CodeQL, clang-tidy, and cppcheck. +- **Fuzzing**: The `kissfft` library is fuzzed with AFL++ to detect edge cases. +- **Vulnerability scanning**: Trivy scans for known vulnerabilities in dependencies. + +--- + +## 📦 Releasing + +Releases are managed by the **Kodi addon maintainers**. To propose a release: + +1. Ensure all changes are merged into the target branch (e.g., `Matrix`). +2. Update the **version number** in `addon.xml.in`. +3. Open a PR with the changes and request a review from maintainers. + +--- + +## 🤝 Community + +- **Discord**: Join the [Kodi Discord](https://kodi.tv/article/kodi-discord-server) for discussions. +- **Forum**: Participate in the [Kodi Forum](https://forum.kodi.tv/). +- **Issues**: Use [GitHub Issues](https://github.com/MarcelRaschke/visualization.matrix/issues) for bug reports and feature requests. + +--- + +## 📄 License + +By contributing to this project, you agree to license your contributions under the **[GPL-2.0-or-later](LICENSE.md)**. + +--- + +## 🙏 Acknowledgments + +Thank you for contributing to **visualization.matrix**! Your efforts help improve the addon for the entire Kodi community. diff --git a/DEPENDENCY_POLICY.md b/DEPENDENCY_POLICY.md new file mode 100644 index 0000000..6b40bb2 --- /dev/null +++ b/DEPENDENCY_POLICY.md @@ -0,0 +1,274 @@ +# Dependency Policy for visualization.matrix + +## 📦 Overview + +This document outlines the **dependency management policy** for the `visualization.matrix` addon. It defines guidelines for: +- **Approved dependencies** +- **Dependency updates** +- **Security requirements** +- **License compatibility** +- **Vulnerability management** + +--- + +## 🔒 Approved Dependencies + +### Current Dependencies + +| Dependency | Version | Purpose | License | Status | +|------------|---------|---------|---------|--------| +| [kissfft](https://github.com/mborgerding/kissfft) | Latest | Fast Fourier Transform | BSD-3-Clause | ✅ Approved | +| [Kodi Addon Framework](https://github.com/xbmc/xbmc) | Matrix/Nexus | Kodi addon infrastructure | GPL-2.0-or-later | ✅ Required | +| OpenGL/ES | System | Graphics rendering | Vendor-specific | ✅ System | +| GLM | Latest | OpenGL Mathematics | MIT | ✅ Approved | + +### Dependency Types + +1. **Bundled Dependencies** (included in `/lib/`) + - Must be **header-only** or **statically linked** + - Must have **compatible licenses** (see [License Compatibility](#license-compatibility)) + - Must be **minimal and well-maintained** + +2. **System Dependencies** (linked at runtime) + - Must be **widely available** on target platforms (Linux, Windows, macOS, Android, iOS) + - Must have **stable APIs** + - Must be **version-checked** at runtime + +3. **Build Dependencies** (used during compilation) + - Must be **documented** in `README.md` or `CONTRIBUTING.md` + - Must be **pinned to specific versions** in CI/CD workflows + +--- + +## 🔄 Dependency Updates + +### Update Process + +1. **Automated Updates (Dependabot)** + - Dependabot automatically opens PRs for dependency updates + - PRs are **reviewed and tested** before merging + - **Security updates** are prioritized (see [Vulnerability Management](#-vulnerability-management)) + +2. **Manual Updates** + - Check for updates **monthly** + - Test updates in a **separate branch** before merging + - Document **breaking changes** in `CHANGELOG.md` (if applicable) + +3. **Version Pinning** + - Use **specific versions** (not `latest` or `*`) + - Pin versions in: + - `CMakeLists.txt` (for bundled dependencies) + - CI/CD workflows (for build dependencies) + - Documentation (for system dependencies) + +### Update Criteria + +| Criteria | Action | +|----------|--------| +| **Security vulnerability** | Update **immediately** (within 48 hours) | +| **Critical bug fix** | Update **within 1 week** | +| **Minor bug fix** | Update **within 1 month** | +| **New feature** | Evaluate for **compatibility and need** | +| **Breaking change** | Requires **major version bump** of the addon | + +--- + +## 🛡️ Security Requirements + +### Vulnerability Management + +1. **Scanning** + - All dependencies are **automatically scanned** with: + - [Trivy](https://github.com/aquasecurity/trivy) (on every push/PR) + - [Dependabot](https://docs.github.com/en/code-security/dependabot) (weekly) + - [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories) (continuous) + +2. **Response to Vulnerabilities** + - **Critical/High vulnerabilities**: Must be fixed **within 48 hours** + - **Medium vulnerabilities**: Must be fixed **within 1 week** + - **Low vulnerabilities**: Must be fixed **within 1 month** + - If a fix is **not available**, apply **mitigations** (e.g., disable vulnerable features) + +3. **Vulnerability Disclosure** + - Follow the **responsible disclosure** process in [SECURITY.md](SECURITY.md) + - Do **not** publicly disclose vulnerabilities before a fix is available + +### Dependency Security Checks + +Before adding a new dependency, verify: + +- [ ] The dependency is **actively maintained** (recent commits, releases) +- [ ] The dependency has **no known vulnerabilities** (check [Snyk](https://snyk.io/), [OSV](https://osv.dev/)) +- [ ] The dependency has a **clear security policy** +- [ ] The dependency uses **secure coding practices** + +--- + +## ⚖️ License Compatibility + +### Approved Licenses + +The `visualization.matrix` addon is licensed under **GPL-2.0-or-later**. Compatible licenses for dependencies include: + +| License | Compatibility | Notes | +|---------|---------------|-------| +| **BSD-2-Clause** | ✅ Compatible | Permissive, no restrictions | +| **BSD-3-Clause** | ✅ Compatible | Permissive, no restrictions | +| **MIT** | ✅ Compatible | Permissive, no restrictions | +| **Apache-2.0** | ✅ Compatible | Permissive, patent grant | +| **LGPL-2.1** | ✅ Compatible | Can be dynamically linked | +| **LGPL-3.0** | ✅ Compatible | Can be dynamically linked | +| **GPL-2.0** | ✅ Compatible | Must be statically linked or bundled | +| **GPL-3.0** | ⚠️ Conditional | Requires addon to be GPL-3.0 | +| **AGPL-3.0** | ❌ Incompatible | Requires network use restrictions | +| **Proprietary** | ❌ Incompatible | Closed-source licenses | + +### License Review Process + +1. **Check License Compatibility** + - Use [SPDX License List](https://spdx.org/licenses/) for reference + - Consult [GPL Compatibility Matrix](https://www.gnu.org/licenses/license-list.html) + +2. **Document License** + - Add the dependency's license to `LICENSE-THIRD-PARTY.md` (if applicable) + - Include a **copy of the license** in `/lib//LICENSE` + +3. **Legal Review** (if unsure) + - Contact the **Kodi legal team** or **project maintainers** for clarification + +--- + +## 📋 Dependency Addition Process + +### Steps to Add a New Dependency + +1. **Evaluate Need** + - Is the dependency **necessary**? + - Can the functionality be **implemented internally**? + - Is there an **existing approved dependency** that can be used? + +2. **Check Compatibility** + - **License**: Must be compatible with GPL-2.0-or-later + - **Platform**: Must support all target platforms (Linux, Windows, macOS, Android, iOS) + - **Architecture**: Must support all target architectures (x86, x86_64, ARM, ARM64) + +3. **Security Review** + - Check for **known vulnerabilities** + - Review the dependency's **security practices** + - Ensure the dependency is **actively maintained** + +4. **Technical Review** + - Test the dependency in a **separate branch** + - Verify **build compatibility** with all target platforms + - Check for **performance impact** + +5. **Documentation** + - Update `README.md` or `CONTRIBUTING.md` with: + - **Purpose** of the dependency + - **Version** requirements + - **Build instructions** (if applicable) + - Update `DEPENDENCY_POLICY.md` with the new dependency + +6. **Submit for Review** + - Open a **Pull Request** with the dependency addition + - Include **justification** for the dependency + - Tag **@MarcelRaschke** for review + +--- + +## 🗑️ Dependency Removal Process + +### Steps to Remove a Dependency + +1. **Evaluate Impact** + - Is the dependency **still needed**? + - Can the functionality be **replaced** with another dependency or internal code? + +2. **Check for Dependencies** + - Are there **other dependencies** that rely on this one? + - Are there **features** that will break without it? + +3. **Update Code** + - Remove all **references** to the dependency + - Replace functionality with **alternatives** (if applicable) + +4. **Test** + - Verify the addon **builds and runs** without the dependency + - Test on **all target platforms** + +5. **Documentation** + - Update `README.md`, `CONTRIBUTING.md`, and `DEPENDENCY_POLICY.md` + - Remove the dependency from **CI/CD workflows** (if applicable) + +6. **Submit for Review** + - Open a **Pull Request** with the dependency removal + - Include **justification** for the removal + - Tag **@MarcelRaschke** for review + +--- + +## 📊 Dependency Metrics + +### Current Dependency Statistics + +| Metric | Value | +|--------|-------| +| **Total Dependencies** | 4 | +| **Bundled Dependencies** | 1 (kissfft) | +| **System Dependencies** | 2 (OpenGL/ES, Kodi Framework) | +| **Build Dependencies** | 1 (CMake) | +| **Vulnerable Dependencies** | 0 | +| **Outdated Dependencies** | 0 | + +### Dependency Health + +- **kissfft**: ✅ Healthy (actively maintained, no vulnerabilities) +- **Kodi Framework**: ✅ Healthy (actively maintained by Kodi team) +- **OpenGL/ES**: ✅ Healthy (vendor-maintained) + +--- + +## 🔄 Automated Dependency Management + +### Dependabot Configuration + +Dependabot is configured to: +- **Check for updates weekly** +- **Open PRs for version updates** +- **Prioritize security updates** +- **Label PRs** with `dependencies` and `security` (if applicable) + +### Trivy Scanning + +Trivy is configured to: +- **Scan for vulnerabilities** on every push/PR +- **Report findings** in SARIF format to GitHub Security tab +- **Fail builds** if critical vulnerabilities are found + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| **Dependency updates** | Monthly | Maintainers | +| **Vulnerability scanning** | Continuous | Automated (Trivy, Dependabot) | +| **Dependency review** | Quarterly | Maintainers | +| **License compliance check** | Annually | Maintainers | + +--- + +## 📞 Contact + +For questions about dependencies, contact: +- **Maintainer**: [@MarcelRaschke](https://github.com/MarcelRaschke) +- **Email**: `security@marcelraschke.de` +- **Kodi Forum**: [Kodi Addon Development](https://forum.kodi.tv/forums/addon-development.161/) + +--- + +## 📜 Changelog + +| Date | Change | Author | +|------|--------|--------| +| 2025-07-15 | Initial dependency policy created | Vibe Code | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e1b1c9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,90 @@ +# Dockerfile for visualization.matrix build environment +# Multi-stage build for smaller final image +# Optimized for CI/CD and production use + +# ============================================ +# Stage 1: Build stage +# ============================================ +FROM ubuntu:22.04 AS builder + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive \ + CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) + +# Install minimal build dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + cmake \ + g++ \ + make \ + git \ + libgl1-mesa-dev \ + libglu1-mesa-dev \ + ninja-build \ + && rm -rf /var/lib/apt/lists/* + +# Clone Kodi source (required for building addons) +WORKDIR /workspace +RUN git clone --depth=1 --branch Matrix https://github.com/xbmc/xbmc.git /kodi + +# Copy source code +COPY . . + +# Build the addon with optimized flags +RUN mkdir -p build && \ + cd build && \ + cmake \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DADDONS_TO_BUILD=visualization.matrix \ + -DADDON_SRC_PREFIX=/workspace \ + -DADDONS_DEFINITION_DIR=/workspace/build/definition \ + -DCMAKE_INSTALL_PREFIX=/workspace/kodi/addons \ + -DPACKAGE_ZIP=1 \ + -DCMAKE_CXX_FLAGS="-O3 -DNDEBUG" \ + -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ + /kodi/cmake/addons && \ + ninja -j$(nproc) + +# ============================================ +# Stage 2: Runtime stage +# ============================================ +FROM ubuntu:22.04 AS runtime + +# Install minimal runtime dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libgl1-mesa-glx \ + libglu1-mesa \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy built addon from builder stage +COPY --from=builder /workspace/kodi/addons /addons + +# Set working directory +WORKDIR /addons + +# Create non-root user for security +RUN useradd -m -u 1000 kodi && \ + chown -R kodi:kodi /addons + +# Switch to non-root user +USER kodi + +# Default command (can be overridden) +CMD ["echo", "visualization.matrix addon built successfully. Files are in /addons"] + +# ============================================ +# Multi-arch build support +# ============================================ +# To build for different architectures, use: +# docker build --platform linux/amd64 -t visualization.matrix:amd64 . +# docker build --platform linux/arm64 -t visualization.matrix:arm64 . +# docker build --platform linux/arm/v7 -t visualization.matrix:armv7 . + +# ============================================ +# Build arguments for customization +# ============================================ +ARG KODI_BRANCH=Matrix +ARG BUILD_TYPE=Release diff --git a/FindOpenGLES.cmake b/FindOpenGLES.cmake deleted file mode 100644 index aaae6d0..0000000 --- a/FindOpenGLES.cmake +++ /dev/null @@ -1,69 +0,0 @@ -#.rst: -# FindOpenGLES -# ------------ -# Finds the OpenGLES2 and OpenGLES3 library -# -# This will define the following variables: -# -# OPENGLES_FOUND - system has OpenGLES -# OPENGLES_INCLUDE_DIRS - the OpenGLES include directory -# OPENGLES_LIBRARIES - the OpenGLES libraries -# OPENGLES_DEFINITIONS - the OpenGLES definitions -# -# Note: -# On Windows with angle the *_INCLUDE_DIRS and -# *_DEFINITIONS are undefined, but are set -# global by the kodi-angle package. - -if(WIN32) - # defined here and not on addon to have it free of OS related 'if' - # and this file as standard for addons where need it - find_package(kodi-angle REQUIRED) - set(OPENGLES_LIBRARIES kodi::angle::libGLESv2 kodi::angle::libEGL) - set(OPENGLES_FOUND ${kodi-angle_FOUND}) - set(OPENGLES_DEFINITIONS -DHAS_GLES=3) -else() - if(CORE_PLATFORM_NAME_LC STREQUAL rbpi) - set(_brcmprefix brcm) - endif() - - if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_OPENGLES ${_brcmprefix}glesv2 QUIET) - endif() - - if(NOT CORE_SYSTEM_NAME STREQUAL ios AND - NOT CORE_SYSTEM_NAME STREQUAL darwin_embedded) - find_path(OPENGLES_INCLUDE_DIR GLES2/gl2.h - PATHS ${PC_OPENGLES_INCLUDEDIR}) - find_library(OPENGLES_gl_LIBRARY NAMES ${_brcmprefix}GLESv2 - PATHS ${PC_OPENGLES_LIBDIR}) - else() - find_library(OPENGLES_gl_LIBRARY NAMES OpenGLES - PATHS ${CMAKE_OSX_SYSROOT}/System/Library - PATH_SUFFIXES Frameworks - NO_DEFAULT_PATH) - set(OPENGLES_INCLUDE_DIR ${OPENGLES_gl_LIBRARY}/Headers) - endif() - - find_path(OPENGLES3_INCLUDE_DIR GLES3/gl3.h) - - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(OpenGLES - REQUIRED_VARS OPENGLES_gl_LIBRARY OPENGLES_INCLUDE_DIR) - - find_path(OPENGLES3_INCLUDE_DIR GLES3/gl3.h - PATHS ${PC_OPENGLES_INCLUDEDIR}) - - if(OPENGLES_FOUND) - set(OPENGLES_LIBRARIES ${OPENGLES_gl_LIBRARY}) - if(OPENGLES3_INCLUDE_DIR) - set(OPENGLES_INCLUDE_DIRS ${OPENGLES_INCLUDE_DIR} ${OPENGLES3_INCLUDE_DIR}) - set(OPENGLES_DEFINITIONS -DHAS_GLES=3) - mark_as_advanced(OPENGLES_INCLUDE_DIR OPENGLES3_INCLUDE_DIR OPENGLES_gl_LIBRARY) - else() - set(OPENGLES_INCLUDE_DIRS ${OPENGLES_INCLUDE_DIR}) - set(OPENGLES_DEFINITIONS -DHAS_GLES=2) - mark_as_advanced(OPENGLES_INCLUDE_DIR OPENGLES_gl_LIBRARY) - endif() - endif() -endif() diff --git a/FindOpenGl.cmake b/FindOpenGl.cmake deleted file mode 100644 index ccde5e5..0000000 --- a/FindOpenGl.cmake +++ /dev/null @@ -1,41 +0,0 @@ -#.rst: -# FindOpenGl -# ---------- -# Finds the FindOpenGl library -# -# This will define the following variables:: -# -# OPENGL_FOUND - system has OpenGl -# OPENGL_INCLUDE_DIRS - the OpenGl include directory -# OPENGL_LIBRARIES - the OpenGl libraries -# OPENGL_DEFINITIONS - the OpenGl definitions - -if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_OPENGL gl QUIET) -endif() - -if(NOT CORE_SYSTEM_NAME STREQUAL osx) - find_path(OPENGL_INCLUDE_DIR GL/gl.h - PATHS ${PC_OPENGL_gl_INCLUDEDIR}) - find_library(OPENGL_gl_LIBRARY NAMES GL - PATHS ${PC_OPENGL_gl_LIBDIR}) -else() - find_library(OPENGL_gl_LIBRARY NAMES OpenGL - PATHS ${CMAKE_OSX_SYSROOT}/System/Library - PATH_SUFFIXES Frameworks - NO_DEFAULT_PATH) - set(OPENGL_INCLUDE_DIR ${OPENGL_gl_LIBRARY}/Headers) -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OpenGl - REQUIRED_VARS OPENGL_gl_LIBRARY OPENGL_INCLUDE_DIR) - -if(OPENGL_FOUND) - set(OPENGL_INCLUDE_DIRS ${OPENGL_INCLUDE_DIR}) - set(OPENGL_LIBRARIES ${OPENGL_gl_LIBRARY}) - set(OPENGL_DEFINITIONS -DHAS_GL=1) -endif() - -mark_as_advanced(OPENGL_INCLUDE_DIR OPENGL_gl_LIBRARY) - diff --git a/Findglm.cmake b/Findglm.cmake deleted file mode 100644 index a21f4ea..0000000 --- a/Findglm.cmake +++ /dev/null @@ -1,25 +0,0 @@ -#.rst: -# Findglm -# ------------ -# Finds the OpenGL Mathematics (GLM) as a header only C++ mathematics library. -# -# This will define the following variables: -# -# GLM_FOUND - system has OpenGLES -# GLM_INCLUDE_DIR - the OpenGLES include directory -# -# Note: Install was removed from GLM on version 0.9.9.6. - -find_package(PkgConfig) -if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_GLM glm QUIET) -endif() - -find_path(GLM_INCLUDE_DIR glm.hpp - PATHS ${PC_GLM_INCLUDEDIR} - PATH_SUFFIXES glm) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(glm REQUIRED_VARS GLM_INCLUDE_DIR) - -mark_as_advanced(GLM_INCLUDE_DIR) diff --git a/README.md b/README.md index 54cc6b1..06d42db 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ This is a [Kodi](https://kodi.tv) visualization addon. [![Build Status](https://jenkins.kodi.tv/view/Addons/job/xbmc/job/visualization.matrix/job/Matrix/badge/icon)](https://jenkins.kodi.tv/blue/organizations/jenkins/xbmc%2Fvisualization.matrix/branches/) +### 🛡️ Security + +This project follows a [Security Policy](SECURITY.md) for responsible vulnerability disclosure. +We use **GitHub CodeQL**, **clang-tidy**, and **cppcheck** for static code analysis to ensure security and quality. + ### Screenshot diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4e4c253 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,64 @@ +# Security Policy for visualization.matrix + +## 🔒 Reporting a Vulnerability + +If you discover a **security vulnerability** in the **visualization.matrix** addon, we encourage you to report it to us **responsibly**. + +### ✅ How to Report +Please **do not** open a public GitHub issue for security vulnerabilities. Instead: + +1. **Send an email** to: + **`security@marcelraschke.de`** + - Include a **detailed description** of the vulnerability. + - Provide **steps to reproduce** the issue. + - If possible, include a **proof-of-concept (PoC)** or exploit code. + +2. **Alternative**: If email is not feasible, you may open a **private GitHub Security Advisory** for this repository. + +### ⏳ Response Time +- We will **acknowledge** your report within **48 hours**. +- We will **investigate** and provide a **fix or mitigation** as soon as possible. +- We aim to **release a patch** within a **reasonable timeframe**, depending on the severity of the issue. + +### 🎯 Scope +This security policy applies **only** to the **visualization.matrix** addon for Kodi. + +- **In Scope**: + - The `visualization.matrix` addon source code. + - Dependencies bundled with the addon (e.g., `kissfft`). + +- **Out of Scope**: + - The **Kodi media center** itself (report to [Kodi Security](https://kodi.tv/about/security)). + - Third-party forks or modified versions of this addon. + +### 🔐 Responsible Disclosure +We follow the principle of **Responsible Disclosure**: + +- **Do not** publicly disclose the vulnerability until we have **released a fix**. +- **Do not** exploit the vulnerability for malicious purposes. +- **Do not** perform **denial-of-service (DoS) attacks** or other disruptive testing without prior approval. + +### 🏆 Recognition +- We will **acknowledge** your contribution in the **release notes** (unless you prefer to remain anonymous). +- This project does **not** currently offer a **bug bounty program**, but we appreciate your responsible disclosure. + +### 📜 Legal +By reporting a vulnerability, you agree to: +- Not share the vulnerability details publicly before a fix is released. +- Not use the vulnerability for any unauthorized or illegal purposes. + +--- + +## 🛡️ Security Features + +This project uses the following security tools to ensure code safety: + +- **[GitHub CodeQL](https://codeql.github.com/)**: Static analysis for C++ to detect vulnerabilities like buffer overflows, memory leaks, and injection flaws. +- **[Dependabot](https://docs.github.com/en/code-security/dependabot)**: Automated dependency scanning for known vulnerabilities. +- **[clang-tidy](https://clang.llvm.org/extra/clang-tidy/)**: Static analysis for C++ code quality and security. +- **[cppcheck](https://cppcheck.sourceforge.io/)**: Additional static analysis for C/C++ code. + +--- + +## 📅 Last Updated +This security policy was last updated on **July 15, 2025**. diff --git a/SHADERS.md b/SHADERS.md new file mode 100644 index 0000000..01d0aae --- /dev/null +++ b/SHADERS.md @@ -0,0 +1,341 @@ +# Shader Documentation for visualization.matrix + +This document describes all **shaders**, **uniforms**, **textures**, and **constants** used in the **Matrix Visualization** addon for Kodi. + +--- + +## 📁 **Shader Files Overview** + +| **Shader File** | **Type** | **Description** | **Preset Name** | **Channel Usage** | +|-----------------|----------|-----------------|-----------------|-------------------| +| [`main_matrix_GL.vert.glsl`](visualization.matrix/resources/shaders/main_matrix_GL.vert.glsl) | Vertex (OpenGL) | Basic vertex shader for OpenGL | All | - | +| [`main_matrix_GLES.vert.glsl`](visualization.matrix/resources/shaders/main_matrix_GLES.vert.glsl) | Vertex (GLES) | Basic vertex shader for OpenGL ES | All | - | +| [`logo.frag.glsl`](visualization.matrix/resources/shaders/logo.frag.glsl) | Fragment | Kodi logo with rain effect | "Kodi" | `iChannel0` (FFT), `iChannel1` (Logo) | +| [`album.frag.glsl`](visualization.matrix/resources/shaders/album.frag.glsl) | Fragment | Album art with rain effect | "Album" | `iChannel0` (FFT), `iChannel3` (Album) | +| [`nologo.frag.glsl`](visualization.matrix/resources/shaders/nologo.frag.glsl) | Fragment | Rain effect only | "Rain only" | `iChannel0` (FFT), `iChannel2` (Noise) | +| [`nologowf.frag.glsl`](visualization.matrix/resources/shaders/nologowf.frag.glsl) | Fragment | Rain with waveform | "Rain with waveform" | `iChannel0` (FFT+Waveform), `iChannel2` (Noise) | +| [`nologowfenv.frag.glsl`](visualization.matrix/resources/shaders/nologowfenv.frag.glsl) | Fragment | Rain with waveform envelope | "Rain with waveform envelope" | `iChannel0` (FFT+Waveform), `iChannel2` (Noise) | +| [`clean.frag.glsl`](visualization.matrix/resources/shaders/clean.frag.glsl) | Fragment | Clean rain effect | "Clean" | `iChannel0` (FFT) | +| [`cleanwf.frag.glsl`](visualization.matrix/resources/shaders/cleanwf.frag.glsl) | Fragment | Clean with waveform | "Clean with waveform" | `iChannel0` (FFT+Waveform) | +| [`cleanwfenv.frag.glsl`](visualization.matrix/resources/shaders/cleanwfenv.frag.glsl) | Fragment | Clean with waveform envelope | "Clean with waveform envelope" | `iChannel0` (FFT+Waveform) | +| [`main_test.frag.glsl`](visualization.matrix/resources/shaders/main_test.frag.glsl) | Fragment | Test shader for bit precision detection | Internal | `iChannel0` (FFT) | + +--- + +## 🎨 **Presets and Channel Mappings** + +The presets are defined in [`src/main.cpp`](src/main.cpp) as follows: + +```cpp +const std::vector g_presets = +{ + {"Kodi", 30100, "logo.frag.glsl", 99, 0, 1, -1}, + {"Album", 30101, "album.frag.glsl", 99, -1, 1, 2}, + {"Rain only", 30102, "nologo.frag.glsl", 99, -1, 1, -1}, + {"Rain with waveform", 30103, "nologowf.frag.glsl", 99, -1, 1, -1}, + {"Rain with waveform envelope", 30104, "nologowfenv.frag.glsl", 99, -1, 1, -1}, + {"Clean", 30105, "clean.frag.glsl", 99, -1, -1, -1}, + {"Clean with waveform", 30106, "cleanwf.frag.glsl", 99, -1, -1, -1}, + {"Clean with waveform envelope", 30107, "cleanwfenv.frag.glsl", 99, -1, -1, -1}, +}; +``` + +### **Channel Mapping Legend** +| **Channel Index** | **Value** | **Meaning** | **Texture/Usage** | +|-------------------|-----------|-------------|-------------------| +| `channel[0]` | `99` | FFT/Audio Data | `iChannel0` (Frequency spectrum + waveform) | +| `channel[1]` | `0` | Logo | `iChannel1` (Kodi logo texture) | +| `channel[1]` | `-1` | Unused | - | +| `channel[2]` | `1` | Noise | `iChannel2` (Noise texture) | +| `channel[2]` | `-1` | Unused | - | +| `channel[3]` | `2` | Album Art | `iChannel3` (Album cover texture) | +| `channel[3]` | `-1` | Unused | - | + +--- + +## 🔧 **Uniform Variables** + +### **Global Uniforms (All Shaders)** + +| **Uniform** | **Type** | **Description** | **Set By** | **Default Value** | +|-------------|----------|-----------------|------------|-------------------| +| `iTime` | `float` | Global time in seconds (scaled by `m_fallSpeed`) | `CVisualizationMatrix::RenderTo()` | `0.0` | +| `iResolution` | `vec2` | Render resolution (width, height) | `GatherDefines()` | `vec2(Width(), Height())` | + +### **Texture Samplers** + +| **Uniform** | **Type** | **Description** | **Used By** | **Texture Source** | +|-------------|----------|-----------------|------------|-------------------| +| `iChannel0` | `sampler2D` | FFT magnitude spectrum + waveform data | All shaders | Dynamically generated from audio | +| `iChannel1` | `sampler2D` | Logo texture | `logo.frag.glsl` | `resources/textures/logo.png` | +| `iChannel2` | `sampler2D` | Noise texture | `nologo.frag.glsl`, `nologowf.frag.glsl`, `nologowfenv.frag.glsl` | `resources/textures/noise.png` | +| `iChannel3` | `sampler2D` | Album cover texture | `album.frag.glsl` | Loaded from `UpdateAlbumart()` | + +### **Album-Specific Uniforms** + +| **Uniform** | **Type** | **Description** | **Used By** | **Set By** | +|-------------|----------|-----------------|------------|------------| +| `iAlbumPosition` | `vec3` | Album position offset (`xy`) and scale (`z`) | `album.frag.glsl` | `CVisualizationMatrix::RenderTo()` | +| `iAlbumRGB` | `vec3` | Album color multiplier (RGB) | `album.frag.glsl` | `CVisualizationMatrix::RenderTo()` | + +--- + +## 🎛 **Constants (Shader Defines)** + +The following constants are injected into the shaders via `GatherDefines()` in [`src/main.cpp`](src/main.cpp): + +### **Visual Style Constants** + +| **Constant** | **Type** | **Description** | **Default Value** | **Configurable** | +|--------------|----------|-----------------|-------------------|------------------| +| `cRNDSEED1` | `float` | Random seed 1 for noise generation | `170.12` | ❌ No | +| `cRNDSEED2` | `float` | Random seed 2 for noise generation | `7572.1` | ❌ No | +| `cINTENSITY` | `float` | Overall brightness intensity | `1.0` | ❌ No | +| `cMININTENSITY` | `float` | Minimum intensity for rain lines | `0.075` | ❌ No | +| `cDISTORTFACTORX` | `float` | Horizontal distortion factor | `0.6` | ❌ No | +| `cDISTORTFACTORY` | `float` | Vertical distortion factor | `0.4` | ❌ No | +| `cVIGNETTEINTENSITY` | `float` | Vignette effect strength | `0.05` | ❌ No | + +### **User-Configurable Constants** + +| **Constant** | **Type** | **Description** | **Source** | **Range** | +|--------------|----------|-----------------|------------|-----------| +| `cDotSize` | `float` | Size of the "dots" (pixels) | `m_dotSize` (Setting: `dotsize`) | `1.0` – `10.0` | +| `cColumns` | `float` | Number of rain columns | Calculated from `Width() / (m_dotSize * 2.0)` | Depends on resolution | +| `cNoiseFluctuation` | `float` | Noise texture fluctuation speed | `m_noiseFluctuation` (Setting: `noisefluctuation`) | `0.0` – `0.1` | +| `cDistortThreshold` | `float` | Threshold for distortion effects | `m_distortThreshold` (Setting: `distortthreshold`) | `0.0` – `0.1` | +| `cRainHighlights` | `float` | Intensity of rain highlights | `m_rainHighlights` (Setting: `rainhighlights`) | `0.0` – `1.0` | +| `cColor` | `vec3` | Color of the rain dots (RGB) | `m_dotColor` (Settings: `red`, `green`, `blue`) | `0.0` – `1.0` per channel | + +--- + +## 📜 **Common Functions (Injected into Shaders)** + +The following functions are injected into all fragment shaders via `fsCommonFunctionsLowPower` or `fsCommonFunctionsNormal` in [`src/main.cpp`](src/main.cpp): + +### **Low-Power Mode Functions** + +```glsl +// Simplified hash function for low-power devices +float h11(float p) +{ + return fract(.13 * p + 217943.37373737 / (p + 0.31)); +} + +// Waveform visualization +float waveform(vec2 uv) +{ + float wave = texture(iChannel0, vec2(uv.x * 0.15 + 0.5, 0.75)).x - 0.5; + return min(abs(uv.y * 20.0 + wave * 10.0), 0.5); +} + +// Noise texture sampling (if dNoise is defined) +#ifdef dNoise +float noise(vec2 gv) +{ + return texture(iChannel2, vec2(gl_FragCoord.xy / (256.0 * cDotSize))).x; +} +#endif + +// Convert brightness to color +vec3 bw2col(float bw, vec2 uv) +{ + float d = length(fract(uv * cColumns) - 0.5); + float peakcolor = 0.6 - d; + float basecolor = 0.8 - d; + return (basecolor * cColor + peakcolor) * bw; +} + +// Get normalized UV coordinates +vec2 getUV() +{ + vec2 uv = (gl_FragCoord.xy - 0.5 * cResolution.xy) / cResolution.y; + return uv; +} +``` + +### **Normal Mode Functions** + +```glsl +// Enhanced hash function for normal mode +float h11(float p) +{ + return fract(20.12345 + sin(p * cRNDSEED1) * cRNDSEED2); +} + +// Waveform visualization (enhanced) +float waveform(vec2 uv) +{ + float wave = texture(iChannel0, vec2(uv.x * 0.15 + 0.5, 0.75)).x * 0.5 + uv.y; + return abs(smoothstep(0.225, 0.275, wave) - 0.5); +} + +// Noise texture sampling (if dNoise is defined) +#ifdef dNoise +float noise(vec2 gv) +{ + return texture(iChannel2, (gv * 0.035431) + iTime * cNoiseFluctuation).x; +} +#endif + +// Convert brightness to color (smooth) +vec3 bw2col(float bw, vec2 uv) +{ + float d = length(fract(uv * cColumns) - 0.5); + float peakcolor = smoothstep(0.35, 0.0, d) * bw; + float basecolor = smoothstep(0.85, 0.0, d) * bw; + return basecolor * cColor + peakcolor; +} + +// Get normalized UV coordinates (with optional CRT curve) +#ifdef dCrtCurve +vec2 getUV() +{ + vec2 uv = (gl_FragCoord.xy - 0.5 * cResolution.xy) / cResolution.y; + uv = uv / (1.0 - length(uv * 0.1)); + return uv; +} +#else +vec2 getUV() +{ + vec2 uv = (gl_FragCoord.xy - 0.5 * cResolution.xy) / cResolution.y; + return uv; +} +#endif +``` + +--- + +## 🔍 **Shader-Specific Details** + +### **1. `album.frag.glsl`** + +**Description:** Renders the album cover with rain effects and distortions. + +**Key Features:** +- Uses `iChannel3` for album cover texture. +- Applies VHS-like distortions based on FFT data (`iChannel0`). +- Uses `iAlbumPosition` and `iAlbumRGB` for positioning and coloring. +- Implements shadow effects and vignette. + +**Uniforms Used:** +- `iTime`, `iResolution` +- `iChannel0` (FFT data) +- `iChannel3` (Album texture) +- `iAlbumPosition` (Position offset and scale) +- `iAlbumRGB` (Color multiplier) + +**Constants Used:** +- `cColumns`, `cDistortThreshold`, `cDISTORTFACTORX`, `cDISTORTFACTORY`, `cRainHighlights`, `cVIGNETTEINTENSITY`, `cINTENSITY`, `cMININTENSITY` + +--- + +### **2. `logo.frag.glsl`** + +**Description:** Renders the Kodi logo with rain effects and distortions. + +**Key Features:** +- Uses `iChannel1` for the Kodi logo texture. +- Applies VHS-like distortions based on FFT data (`iChannel0`). +- Implements interlaced logo distortion effect. + +**Uniforms Used:** +- `iTime`, `iResolution` +- `iChannel0` (FFT data) +- `iChannel1` (Logo texture) + +**Constants Used:** +- `cColumns`, `cDistortThreshold`, `cDISTORTFACTORX`, `cDISTORTFACTORY`, `cRainHighlights`, `cVIGNETTEINTENSITY` + +--- + +### **3. `nologo.frag.glsl`** + +**Description:** Renders rain effect only (no logo or album). + +**Key Features:** +- Pure rain effect with noise texture. +- Uses FFT data for distortion effects. +- Implements vignette effect. + +**Uniforms Used:** +- `iTime`, `iResolution` +- `iChannel0` (FFT data) +- `iChannel2` (Noise texture) + +**Constants Used:** +- `cColumns`, `cNoiseFluctuation`, `cRainHighlights`, `cVIGNETTEINTENSITY`, `cINTENSITY` + +--- + +### **4. `nologowf.frag.glsl` & `nologowfenv.frag.glsl`** + +**Description:** Rain effect with waveform visualization. + +**Key Features:** +- Combines rain effect with waveform data from `iChannel0`. +- `nologowfenv.frag.glsl` uses waveform envelope for smoother transitions. + +**Uniforms Used:** +- `iTime`, `iResolution` +- `iChannel0` (FFT + waveform data) +- `iChannel2` (Noise texture) + +**Constants Used:** +- `cColumns`, `cNoiseFluctuation`, `cRainHighlights`, `cVIGNETTEINTENSITY`, `cINTENSITY` + +--- + +### **5. `clean.frag.glsl`, `cleanwf.frag.glsl`, `cleanwfenv.frag.glsl`** + +**Description:** Clean rain effect without logo or album. + +**Key Features:** +- Minimalistic rain effect. +- `cleanwf.frag.glsl` adds waveform visualization. +- `cleanwfenv.frag.glsl` uses waveform envelope. + +**Uniforms Used:** +- `iTime`, `iResolution` +- `iChannel0` (FFT + waveform data) + +**Constants Used:** +- `cColumns`, `cRainHighlights` + +--- + +## 🛠 **Texture Files** + +| **Texture File** | **Path** | **Usage** | **Format** | **Dimensions** | +|------------------|----------|-----------|------------|----------------| +| `logo.png` | `resources/textures/logo.png` | Kodi logo | RGBA | 512×512 | +| `noise.png` | `resources/textures/noise.png` | Noise texture | RGBA | 256×256 | + +--- + +## 📊 **Performance Considerations** + +1. **Texture Uploads:** + - Audio data textures (`iChannel0`) are updated every frame via `glTexImage2D`. + - Album textures (`iChannel3`) are only updated when the album art changes. + +2. **Shader Complexity:** + - Low-power mode uses simplified functions (`fsCommonFunctionsLowPower`). + - Normal mode uses more complex functions (`fsCommonFunctionsNormal`). + +3. **Preset Switching:** + - Switching presets reloads the shader and reconfigures the uniforms. + - Textures are reused where possible. + +4. **Framebuffer:** + - A framebuffer is used for rendering to a texture before displaying. + - Resolution is configurable via `m_state.fbwidth` and `m_state.fbheight`. + +--- + +## 🔗 **See Also** + +- [Main Source Code (`src/main.cpp`)](src/main.cpp) +- [Header File (`src/main.h`)](src/main.h) +- [Kodi Visualization API](https://kodi.tv) +- [GLSL Reference](https://www.khronos.org/opengl/) diff --git a/playlist-website/index.html b/playlist-website/index.html new file mode 100644 index 0000000..b1c255c --- /dev/null +++ b/playlist-website/index.html @@ -0,0 +1,444 @@ + + + + + +Eurodance & French Touch Time Capsule — Extended Mixes + + + + +
+

🎧 Eurodance & French Touch Time Capsule

+

Extended Mixes Only · 1988–2010 · 45 Tracks · 15 Länder · 31 Künstler

+
+
45
Tracks
+
15
Länder
+
31
Künstler
+
8
Phasen
+
22
Jahre
+
+
+ +
+ + + + + +
+ +
+ +
+

Playlist kuratiert & verifiziert · Alle Links via Spotify/Discogs · Extended Mixes für DJ-Sets

+
+ + + + diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..86295af --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,38 @@ +# SonarQube configuration for visualization.matrix +# https://docs.sonarqube.org/latest/analysis/analysis-parameters/ + +# Project identification +sonar.projectKey=visualization.matrix +sonar.projectName=visualization.matrix +sonar.projectVersion=1.0 + +# Source files +sonar.sources=src,lib +sonar.exclusions=**/build/**,**/.git/**,**/visualization.matrix/** + +# Language settings +sonar.language=c++ +sonar.cxx.standard=c++17 +sonar.cxx.compiler=gcc + +# Encoding +sonar.sourceEncoding=UTF-8 + +# Coverage +sonar.cxx.coverage.reportPaths=build/coverage.info +sonar.cxx.coverage.forceDeprecatedJavaVersion=false + +# External reports +sonar.cxx.clang-tidy.report-path=build/clang-tidy-report.txt +sonar.cxx.cppcheck.report-path=build/cppcheck-report.txt + +# Quality profiles +sonar.profile=Sonar way + +# Rules +sonar.cxx.valgrind.reportPath=build/valgrind-report.xml +sonar.cxx.xunit.reportPath=build/test-results.xml + +# Security +sonar.security.hotspot.review.enabled=true +sonar.security.scan.type=FULL diff --git a/src/main.cpp b/src/main.cpp index 49d6ead..294dc0b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,19 +17,14 @@ #include "kodi/Filesystem.h" #include "kodi/General.h" - - -#define _USE_MATH_DEFINES #include #include -#include +#include +#include -#define SMOOTHING_TIME_CONSTANT (0.5) // default 0.8 -#define MIN_DECIBELS (-100.0) -#define MAX_DECIBELS (-30.0) - -#define AUDIO_BUFFER (1024) -#define NUM_BANDS (AUDIO_BUFFER / 2) +#define SMOOTHING_TIME_CONSTANT (0.5f) // default 0.8 +#define MIN_DECIBELS (-100.0f) +#define MAX_DECIBELS (-30.0f) // Override GL_RED if not present with GL_LUMINANCE, e.g. on Android GLES #ifndef GL_RED @@ -56,6 +51,8 @@ const std::vector g_presets = {"Clean", 30105, "clean.frag.glsl", 99, -1, -1, -1}, {"Clean with waveform", 30106, "cleanwf.frag.glsl", 99, -1, -1, -1}, {"Clean with waveform envelope", 30107, "cleanwfenv.frag.glsl", 99, -1, -1, -1}, + {"Neon", 30108, "neon.frag.glsl", 99, -1, -1, -1}, + {"Odysseus", 30109, "odysseus.frag.glsl", 99, -1, -1, -1}, }; const std::vector g_fileTextures = @@ -144,10 +141,10 @@ vec2 getUV() )functions"; CVisualizationMatrix::CVisualizationMatrix() - : m_kissCfg(kiss_fft_alloc(AUDIO_BUFFER, 0, nullptr, nullptr)), - m_audioData(new GLubyte[AUDIO_BUFFER]()), - m_magnitudeBuffer(new float[NUM_BANDS]()), - m_pcm(new float[AUDIO_BUFFER]()) + : m_kissCfg(kiss_fft_alloc(static_cast(AUDIO_BUFFER), 0, nullptr, nullptr)), + m_audioData(AUDIO_BUFFER), + m_magnitudeBuffer(NUM_BANDS), + m_pcm(AUDIO_BUFFER) { m_currentPreset = kodi::GetSettingInt("lastpresetidx"); m_dotMode = static_cast(kodi::GetSettingBoolean("dotmode")); @@ -157,27 +154,26 @@ CVisualizationMatrix::CVisualizationMatrix() } else { - if (Height() <= 900) m_dotSize = 3.; - else if (Height() <= 1500) m_dotSize = 4.; - else m_dotSize = 5.; + if (Height() <= 900) m_dotSize = 3.0f; + else if (Height() <= 1500) m_dotSize = 4.0f; + else m_dotSize = 5.0f; } - m_fallSpeed = static_cast(kodi::GetSettingInt("fallspeed")) * .01; - m_distortThreshold = static_cast(kodi::GetSettingInt("distortthreshold")) * .005; - m_rainHighlights = static_cast(kodi::GetSettingInt("rainhighlights")) * .016; - m_dotColor.red = static_cast(kodi::GetSettingInt("red")) / 255.f; - m_dotColor.green = static_cast(kodi::GetSettingInt("green")) / 255.f; - m_dotColor.blue = static_cast(kodi::GetSettingInt("blue")) / 255.f; + m_fallSpeed = static_cast(kodi::GetSettingInt("fallspeed")) * 0.01f; + m_distortThreshold = static_cast(kodi::GetSettingInt("distortthreshold")) * 0.005f; + m_rainHighlights = static_cast(kodi::GetSettingInt("rainhighlights")) * 0.016f; + m_dotColor.red = static_cast(kodi::GetSettingInt("red")) / 255.0f; + m_dotColor.green = static_cast(kodi::GetSettingInt("green")) / 255.0f; + m_dotColor.blue = static_cast(kodi::GetSettingInt("blue")) / 255.0f; m_lowpower = kodi::GetSettingBoolean("lowpower"); - m_noiseFluctuation = m_lowpower ? (static_cast(kodi::GetSettingInt("noisefluctuation")) * 0.0002f)/m_fallSpeed * 0.25f : (static_cast(kodi::GetSettingInt("noisefluctuation")) * 0.0004f)/m_fallSpeed * 0.25f; + m_noiseFluctuation = m_lowpower ? (static_cast(kodi::GetSettingInt("noisefluctuation")) * 0.0002f)/m_fallSpeed * 0.25f + : (static_cast(kodi::GetSettingInt("noisefluctuation")) * 0.0004f)/m_fallSpeed * 0.25f; m_crtCurve = kodi::GetSettingBoolean("crtcurve"); m_lastAlbumChange = 0.0; } CVisualizationMatrix::~CVisualizationMatrix() { - delete [] m_audioData; - delete [] m_magnitudeBuffer; - delete [] m_pcm; + // Vectors are automatically cleaned up free(m_kissCfg); } @@ -199,10 +195,10 @@ bool CVisualizationMatrix::Start(int iChannels, int iSamplesPerSec, int iBitsPer //background vertex static const GLfloat vertex_data[] = { - -1.0, 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, 1.0, - 1.0,-1.0, 1.0, 1.0, - -1.0,-1.0, 1.0, 1.0, + -1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f,-1.0f, 1.0f, 1.0f, + -1.0f,-1.0f, 1.0f, 1.0f, }; // Upload vertex data to a buffer @@ -228,38 +224,41 @@ void CVisualizationMatrix::Stop() glDeleteBuffers(1, &m_state.vertex_buffer); } - void CVisualizationMatrix::AudioData(const float* pAudioData, int iAudioDataLength, float* pFreqData, int iFreqDataLength) { - WriteToBuffer(pAudioData, iAudioDataLength, 2); + WriteToBuffer(pAudioData, static_cast(iAudioDataLength), 2); - kiss_fft_cpx in[AUDIO_BUFFER], out[AUDIO_BUFFER]; - for (unsigned int i = 0; i < AUDIO_BUFFER; i++) + std::vector in(AUDIO_BUFFER); + std::vector out(AUDIO_BUFFER); + + for (size_t i = 0; i < AUDIO_BUFFER; i++) { in[i].r = BlackmanWindow(m_pcm[i], i, AUDIO_BUFFER); in[i].i = 0; } - kiss_fft(m_kissCfg, in, out); + kiss_fft(m_kissCfg, in.data(), out.data()); out[0].i = 0; - SmoothingOverTime(m_magnitudeBuffer, m_magnitudeBuffer, out, NUM_BANDS, SMOOTHING_TIME_CONSTANT, AUDIO_BUFFER); + // Create temporary copy for smoothing + std::vector tempBuffer = m_magnitudeBuffer; + SmoothingOverTime(m_magnitudeBuffer, tempBuffer, out.data(), NUM_BANDS, SMOOTHING_TIME_CONSTANT, static_cast(AUDIO_BUFFER)); const double rangeScaleFactor = MAX_DECIBELS == MIN_DECIBELS ? 1 : (1.0 / (MAX_DECIBELS - MIN_DECIBELS)); - for (unsigned int i = 0; i < NUM_BANDS; i++) + for (size_t i = 0; i < NUM_BANDS; i++) { float linearValue = m_magnitudeBuffer[i]; double dbMag = !linearValue ? MIN_DECIBELS : LinearToDecibels(linearValue); double scaledValue = UCHAR_MAX * (dbMag - MIN_DECIBELS) * rangeScaleFactor; - m_audioData[i] = std::max(std::min((int)scaledValue, UCHAR_MAX), 0); + m_audioData[i] = static_cast(std::max(std::min(static_cast(scaledValue), UCHAR_MAX), 0)); } - for (unsigned int i = 0; i < NUM_BANDS; i++) + for (size_t i = 0; i < NUM_BANDS; i++) { float v = (m_pcm[i] + 1.0f) * 128.0f; - m_audioData[i + NUM_BANDS] = std::max(std::min((int)v, UCHAR_MAX), 0); + m_audioData[i + NUM_BANDS] = static_cast(std::max(std::min(static_cast(v), UCHAR_MAX), 0)); } m_needsUpload = true; @@ -270,7 +269,7 @@ void CVisualizationMatrix::AudioData(const float* pAudioData, int iAudioDataLeng //----------------------------------------------------------------------------- bool CVisualizationMatrix::NextPreset() { - m_currentPreset = (m_currentPreset + 1) % g_presets.size(); + m_currentPreset = (m_currentPreset + 1) % static_cast(g_presets.size()); Launch(m_currentPreset); UpdateAlbumart(); kodi::SetSettingInt("lastpresetidx", m_currentPreset); @@ -279,7 +278,7 @@ bool CVisualizationMatrix::NextPreset() bool CVisualizationMatrix::PrevPreset() { - m_currentPreset = (m_currentPreset - 1) % g_presets.size(); + m_currentPreset = (m_currentPreset - 1 + static_cast(g_presets.size())) % static_cast(g_presets.size()); Launch(m_currentPreset); UpdateAlbumart(); kodi::SetSettingInt("lastpresetidx", m_currentPreset); @@ -288,8 +287,8 @@ bool CVisualizationMatrix::PrevPreset() bool CVisualizationMatrix::LoadPreset(int select) { - kodi::Log(ADDON_LOG_DEBUG, "Loading preset %i\n",select); - m_currentPreset = select % g_presets.size(); + kodi::Log(ADDON_LOG_DEBUG, "Loading preset %i\n", select); + m_currentPreset = select % static_cast(g_presets.size()); Launch(m_currentPreset); UpdateAlbumart(); kodi::SetSettingInt("lastpresetidx", m_currentPreset); @@ -298,7 +297,7 @@ bool CVisualizationMatrix::LoadPreset(int select) bool CVisualizationMatrix::RandomPreset() { - m_currentPreset = (int)((std::rand() / (float)RAND_MAX) * g_presets.size()); + m_currentPreset = static_cast((std::rand() / static_cast(RAND_MAX)) * g_presets.size()); Launch(m_currentPreset); UpdateAlbumart(); kodi::SetSettingInt("lastpresetidx", m_currentPreset); @@ -311,7 +310,7 @@ bool CVisualizationMatrix::RandomPreset() bool CVisualizationMatrix::GetPresets(std::vector& presets) { std::string name; - for (auto preset : g_presets) + for (const auto& preset : g_presets) { name = kodi::GetLocalizedString(preset.labelId, preset.name); presets.push_back(name); @@ -329,22 +328,22 @@ int CVisualizationMatrix::GetActivePreset() bool CVisualizationMatrix::UpdateAlbumart() { - return CVisualizationMatrix::UpdateAlbumart(m_albumArt); + return UpdateAlbumart(m_albumArt); } bool CVisualizationMatrix::UpdateAlbumart(std::string albumart) { - m_albumArt = albumart; + m_albumArt = std::move(albumart); - kodi::Log(ADDON_LOG_DEBUG, "Updating album art %s\n",albumart.c_str()); + kodi::Log(ADDON_LOG_DEBUG, "Updating album art %s\n", m_albumArt.c_str()); if (g_presets[m_currentPreset].channel[3] != 2) { return false; } - std::string thumb = kodi::vfs::GetCacheThumbName(albumart.c_str()); - thumb = thumb.substr(0,8); - std::string special = std::string("special://thumbnails/") + thumb.c_str()[0] + std::string("/") + thumb.c_str(); + std::string thumb = kodi::vfs::GetCacheThumbName(m_albumArt.c_str()); + thumb = thumb.substr(0, 8); + std::string special = std::string("special://thumbnails/") + thumb[0] + std::string("/") + thumb; if (kodi::vfs::FileExists(special + std::string(".png"))) { @@ -371,10 +370,13 @@ void CVisualizationMatrix::RenderTo(GLuint shader, GLuint effect_fb) GLuint w = Width(); GLuint h = Height(); if (m_state.fbwidth && m_state.fbheight) - w = m_state.fbwidth, h = m_state.fbheight; - int64_t intt = static_cast(std::chrono::duration(std::chrono::high_resolution_clock::now().time_since_epoch()).count() * 1000.0 * m_fallSpeed) - m_initialTime; + w = static_cast(m_state.fbwidth), h = static_cast(m_state.fbheight); + + const auto now = std::chrono::high_resolution_clock::now(); + const auto duration = std::chrono::duration(now.time_since_epoch()); + int64_t intt = static_cast(duration.count() * 1000.0 * m_fallSpeed) - m_initialTime; if (m_bitsPrecision) - intt &= (1<(NUM_BANDS), 2, 0, GL_RED, GL_UNSIGNED_BYTE, m_audioData.data()); } } m_needsUpload = false; - if (g_presets[m_currentPreset].channel[3] == 2) { - double logotimer = std::chrono::duration(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); - float delta = static_cast(logotimer - m_lastAlbumChange)*0.6f; - GLfloat r = std::max(static_cast(sin(delta)),0.0f)*0.7f; - GLfloat g = std::max(static_cast(sin(delta - 1.0f)),0.0f)*0.7f; - GLfloat b = std::max(static_cast(sin(delta - 2.0f)),0.0f)*0.7f; + const auto logotimer = std::chrono::duration(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + const float delta = static_cast(logotimer - m_lastAlbumChange) * 0.6f; + const GLfloat r = std::max(std::sin(delta), 0.0f) * 0.7f; + const GLfloat g = std::max(std::sin(delta - 1.0f), 0.0f) * 0.7f; + const GLfloat b = std::max(std::sin(delta - 2.0f), 0.0f) * 0.7f; glUniform3f(m_attrAlbumRGBLoc, r, g, b); if (m_lastAlbumChange == 0.0) { - glUniform3f(m_attrAlbumPositionLoc, 0.f, 0.f, 2.0f); + glUniform3f(m_attrAlbumPositionLoc, 0.0f, 0.0f, 2.0f); } - if (logotimer - m_lastAlbumChange >= 10.) + if (logotimer - m_lastAlbumChange >= 10.0) { - m_albumX = static_cast(std::fmod(logotimer * 1234., 1.) * (static_cast(Width())/static_cast(Height()) + 1.) - 1.); - m_albumY = static_cast(std::fmod(logotimer * 7654., 1.)); + m_albumX = static_cast(std::fmod(logotimer * 1234.0, 1.0) * (static_cast(Width())/static_cast(Height()) + 1.0) - 1.0); + m_albumY = static_cast(std::fmod(logotimer * 7654.0, 1.0)); m_lastAlbumChange = logotimer; m_AlbumNeedsUpload = true; } if (m_AlbumNeedsUpload) { - glUniform3f(m_attrAlbumPositionLoc, m_albumX, m_albumY, 2.0f);//FIXME: proper framing, the album can reach over the edge of the screen - m_AlbumNeedsUpload = true;//FIXME: limit upload to the actual album shader + // Clamp album position to ensure it stays within screen bounds [-1.0, 1.0] + m_albumX = std::max(-1.0f, std::min(1.0f, m_albumX)); + m_albumY = std::max(-1.0f, std::min(1.0f, m_albumY)); + glUniform3f(m_attrAlbumPositionLoc, m_albumX, m_albumY, 2.0f); + m_AlbumNeedsUpload = false; // Fixed: was always true } } } - float t = intt / 1000.0f; + const float t = static_cast(intt) / 1000.0f; glUniform1f(m_attrGlobalTimeLoc, t); @@ -439,7 +443,7 @@ void CVisualizationMatrix::RenderTo(GLuint shader, GLuint effect_fb) glBindFramebuffer(GL_FRAMEBUFFER, effect_fb); glBindBuffer(GL_ARRAY_BUFFER, m_state.vertex_buffer); - glVertexAttribPointer(m_state.attr_vertex_e, 4, GL_FLOAT, 0, 16, 0); + glVertexAttribPointer(m_state.attr_vertex_e, 4, GL_FLOAT, GL_FALSE, 16, nullptr); glEnableVertexAttribArray(m_state.attr_vertex_e); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); glDisableVertexAttribArray(m_state.attr_vertex_e); @@ -456,8 +460,8 @@ void CVisualizationMatrix::RenderTo(GLuint shader, GLuint effect_fb) void CVisualizationMatrix::Mix(float* destination, const float* source, size_t frames, size_t channels) { - size_t length = frames * channels; - for (unsigned int i = 0; i < length; i += channels) + const size_t length = frames * channels; + for (size_t i = 0; i < length; i += channels) { float v = 0.0f; for (size_t j = 0; j < channels; j++) @@ -465,43 +469,46 @@ void CVisualizationMatrix::Mix(float* destination, const float* source, size_t f v += source[i + j]; } - destination[(i / 2)] = v / (float)channels; + destination[(i / 2)] = v / static_cast(channels); } } void CVisualizationMatrix::WriteToBuffer(const float* input, size_t length, size_t channels) { - size_t frames = length / channels; + const size_t frames = length / channels; if (frames >= AUDIO_BUFFER) { - size_t offset = frames - AUDIO_BUFFER; + const size_t offset = frames - AUDIO_BUFFER; - Mix(m_pcm, input + offset, AUDIO_BUFFER, channels); + Mix(m_pcm.data(), input + offset, AUDIO_BUFFER, channels); } else { - size_t keep = AUDIO_BUFFER - frames; - memmove(m_pcm, m_pcm + frames, keep * sizeof(float)); + const size_t keep = AUDIO_BUFFER - frames; + std::copy(m_pcm.begin() + frames, m_pcm.end(), m_pcm.begin()); - Mix(m_pcm + keep, input, frames, channels); + Mix(m_pcm.data() + keep, input, frames, channels); } } void CVisualizationMatrix::Launch(int preset) { m_bitsPrecision = DetermineBitsPrecision(); - // mali-400 has only 10 bits which means milliseond timer wraps after ~1 second. + // mali-400 has only 10 bits which means millisecond timer wraps after ~1 second. // we'll fudge that up a bit as having a larger range is more important than ms accuracy m_bitsPrecision = std::max(m_bitsPrecision, 13); kodi::Log(ADDON_LOG_DEBUG, "bits of precision: %d", m_bitsPrecision); UnloadTextures(); + // Reset album upload flag - only set to true for album shader + m_AlbumNeedsUpload = (g_presets[preset].channel[3] == 2); + m_usedShaderFile = kodi::GetAddonPath("resources/shaders/" + g_presets[preset].file); for (int i = 0; i < 4; i++) { - if (g_presets[preset].channel[i] >= 0 && g_presets[preset].channel[i] < static_cast< int > (g_fileTextures.size())) + if (g_presets[preset].channel[i] >= 0 && g_presets[preset].channel[i] < static_cast(g_fileTextures.size())) { m_shaderTextures[i].texture = kodi::GetAddonPath("resources/textures/" + g_fileTextures[g_presets[preset].channel[i]]); } @@ -511,12 +518,12 @@ void CVisualizationMatrix::Launch(int preset) } else { - m_shaderTextures[i].texture = ""; + m_shaderTextures[i].texture.clear(); m_shaderTextures[i].audio = false; } } // Audio - m_channelTextures[0] = CreateTexture(GL_RED, NUM_BANDS, 2, m_audioData); + m_channelTextures[0] = CreateTexture(GL_RED, static_cast(NUM_BANDS), 2, m_audioData.data()); // Logo if (!m_shaderTextures[1].texture.empty()) { @@ -554,7 +561,7 @@ void CVisualizationMatrix::LoadPreset(const std::string& shaderPath) { UnloadPreset(); GatherDefines(); - std::string vertMatrixShader = kodi::GetAddonPath("resources/shaders/main_matrix_" GL_TYPE_STRING ".vert.glsl"); + const std::string vertMatrixShader = kodi::GetAddonPath("resources/shaders/main_matrix_" GL_TYPE_STRING ".vert.glsl"); if (!m_matrixShader.LoadShaderFiles(vertMatrixShader, shaderPath) || !m_matrixShader.CompileAndLink("", "", m_defines, "")) { @@ -562,7 +569,7 @@ void CVisualizationMatrix::LoadPreset(const std::string& shaderPath) return; } - GLuint matrixShader = m_matrixShader.ProgramHandle(); + const GLuint matrixShader = m_matrixShader.ProgramHandle(); m_attrGlobalTimeLoc = glGetUniformLocation(matrixShader, "iTime"); m_attrAlbumPositionLoc = glGetUniformLocation(matrixShader, "iAlbumPosition"); @@ -578,7 +585,7 @@ void CVisualizationMatrix::LoadPreset(const std::string& shaderPath) glActiveTexture(GL_TEXTURE0); glGenTextures(1, &m_state.framebuffer_texture); glBindTexture(GL_TEXTURE_2D, m_state.framebuffer_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_state.fbwidth, m_state.fbheight, 0, GL_RGB, GL_UNSIGNED_BYTE, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_state.fbwidth, m_state.fbheight, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -588,7 +595,9 @@ void CVisualizationMatrix::LoadPreset(const std::string& shaderPath) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_state.framebuffer_texture, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0); - m_initialTime = static_cast(std::chrono::duration(std::chrono::high_resolution_clock::now().time_since_epoch()).count() * 1000.0); + const auto now = std::chrono::high_resolution_clock::now(); + const auto duration = std::chrono::duration(now.time_since_epoch()); + m_initialTime = static_cast(duration.count() * 1000.0); m_initialTime += (m_initialTime % 100000); } @@ -645,18 +654,19 @@ GLuint CVisualizationMatrix::CreateTexture(const std::string& file, GLint intern { kodi::Log(ADDON_LOG_DEBUG, "creating texture %s\n", file.c_str()); - int width,height,n; - unsigned char* image; + int width = 0, height = 0, n = 0; + unsigned char* image = nullptr; stbi_set_flip_vertically_on_load(true); - image = stbi_load(file.c_str(), &height, &width, &n, STBI_rgb_alpha); + image = stbi_load(file.c_str(), &width, &height, &n, STBI_rgb_alpha); if (image == nullptr) { kodi::Log(ADDON_LOG_ERROR, "couldn't load image"); return 0; - } + } - GLuint texture = CreateTexture(image, GL_RGBA, width, height, internalFormat, scaling, repeat); + const GLuint texture = CreateTexture(image, GL_RGBA, static_cast(width), + static_cast(height), internalFormat, scaling, repeat); stbi_image_free(image); image = nullptr; @@ -665,60 +675,61 @@ GLuint CVisualizationMatrix::CreateTexture(const std::string& file, GLint intern float CVisualizationMatrix::BlackmanWindow(float in, size_t i, size_t length) { - double alpha = 0.16; - double a0 = 0.5 * (1.0 - alpha); - double a1 = 0.5; - double a2 = 0.5 * alpha; + constexpr double alpha = 0.16; + constexpr double a0 = 0.5 * (1.0 - alpha); + constexpr double a1 = 0.5; + constexpr double a2 = 0.5 * alpha; - float x = (float)i / (float)length; - return in * (a0 - a1 * cos(2.0 * M_PI * x) + a2 * cos(4.0 * M_PI * x)); + const float x = static_cast(i) / static_cast(length); + return in * static_cast(a0 - a1 * std::cos(2.0 * M_PI * x) + a2 * std::cos(4.0 * M_PI * x)); } -void CVisualizationMatrix::SmoothingOverTime(float* outputBuffer, float* lastOutputBuffer, kiss_fft_cpx* inputBuffer, size_t length, float smoothingTimeConstant, unsigned int fftSize) +void CVisualizationMatrix::SmoothingOverTime(std::vector& outputBuffer, const std::vector& lastOutputBuffer, + kiss_fft_cpx* inputBuffer, size_t length, float smoothingTimeConstant, unsigned int fftSize) { for (size_t i = 0; i < length; i++) { - kiss_fft_cpx c = inputBuffer[i]; - float magnitude = sqrt(c.r * c.r + c.i * c.i) / (float)fftSize; - outputBuffer[i] = smoothingTimeConstant * lastOutputBuffer[i] + (1.0 - smoothingTimeConstant) * magnitude; + const kiss_fft_cpx c = inputBuffer[i]; + const float magnitude = std::sqrt(c.r * c.r + c.i * c.i) / static_cast(fftSize); + outputBuffer[i] = smoothingTimeConstant * lastOutputBuffer[i] + (1.0f - smoothingTimeConstant) * magnitude; } } float CVisualizationMatrix::LinearToDecibels(float linear) { if (!linear) - return -1000; - return 20 * log10f(linear); + return -1000.0f; + return 20.0f * std::log10(linear); } int CVisualizationMatrix::DetermineBitsPrecision() { - m_state.fbwidth = 32, m_state.fbheight = 26*10; + m_state.fbwidth = 32; + m_state.fbheight = 26 * 10; LoadPreset(kodi::GetAddonPath("resources/shaders/main_test.frag.glsl")); RenderTo(m_matrixShader.ProgramHandle(), m_state.effect_fb); glFinish(); - unsigned char* buffer = new unsigned char[m_state.fbwidth * m_state.fbheight * 4]; + std::unique_ptr buffer(new unsigned char[m_state.fbwidth * m_state.fbheight * 4]); if (buffer) - glReadPixels(0, 0, m_state.fbwidth, m_state.fbheight, GL_RGBA, GL_UNSIGNED_BYTE, buffer); + glReadPixels(0, 0, m_state.fbwidth, m_state.fbheight, GL_RGBA, GL_UNSIGNED_BYTE, buffer.get()); int bits = 0; unsigned char b = 0; - for (int j=0; j>1))]; + const unsigned char c = buffer[4 * (j * m_state.fbwidth + (m_state.fbwidth >> 1))]; if (c && !b) bits++; b = c; } - delete buffer; UnloadPreset(); return bits; } void CVisualizationMatrix::GatherDefines() { - m_defines = ""; + m_defines.clear(); #if defined(HAS_GL) m_defines += "#version 150\n"; m_defines += "#extension GL_OES_standard_derivatives : enable\n"; @@ -742,11 +753,12 @@ void CVisualizationMatrix::GatherDefines() m_defines += "const float cVIGNETTEINTENSITY = 0.05;\n"; m_defines += "const float cDotSize = " + std::to_string(m_dotSize) + ";\n"; - m_defines += "const float cColumns = " + std::to_string(static_cast(Width())/(m_dotSize*2.0)) + ";\n"; + m_defines += "const float cColumns = " + std::to_string(static_cast(Width()) / (m_dotSize * 2.0f)) + ";\n"; m_defines += "const float cNoiseFluctuation = " + std::to_string(m_noiseFluctuation) + ";\n"; m_defines += "const float cDistortThreshold = " + std::to_string(m_distortThreshold) + ";\n"; m_defines += "const float cRainHighlights = " + std::to_string(m_rainHighlights) + ";\n"; - m_defines += "const vec3 cColor = vec3(" + std::to_string(m_dotColor.red) + "," + std::to_string(m_dotColor.green) + "," + std::to_string(m_dotColor.blue) + ");\n"; + m_defines += "const vec3 cColor = vec3(" + std::to_string(m_dotColor.red) + "," + + std::to_string(m_dotColor.green) + "," + std::to_string(m_dotColor.blue) + ");\n"; if (m_state.fbwidth && m_state.fbheight) { @@ -797,7 +809,7 @@ void CVisualizationMatrix::GatherDefines() m_defines += fsCommonFunctionsNormal; } - kodi::Log(ADDON_LOG_DEBUG, "Fragment shader header\n%s",m_defines.c_str()); + kodi::Log(ADDON_LOG_DEBUG, "Fragment shader header\n%s", m_defines.c_str()); } ADDONCREATOR(CVisualizationMatrix) // Don't touch this! diff --git a/src/main.h b/src/main.h index f5b507f..11ab420 100644 --- a/src/main.h +++ b/src/main.h @@ -13,7 +13,11 @@ #include #include -#include "kissfft/kiss_fft.h" +#include + +#include +#include +#include class ATTRIBUTE_HIDDEN CVisualizationMatrix : public kodi::addon::CAddonBase @@ -23,6 +27,12 @@ class ATTRIBUTE_HIDDEN CVisualizationMatrix CVisualizationMatrix(); ~CVisualizationMatrix() override; + // Disable copy and move + CVisualizationMatrix(const CVisualizationMatrix&) = delete; + CVisualizationMatrix& operator=(const CVisualizationMatrix&) = delete; + CVisualizationMatrix(CVisualizationMatrix&&) = delete; + CVisualizationMatrix& operator=(CVisualizationMatrix&&) = delete; + bool Start(int channels, int samplesPerSec, int bitsPerSample, std::string songName) override; void Stop() override; void AudioData(const float* audioData, int audioDataLength, float* freqData, int freqDataLength) override; @@ -47,76 +57,77 @@ class ATTRIBUTE_HIDDEN CVisualizationMatrix GLuint CreateTexture(const GLvoid* data, GLint format, unsigned int w, unsigned int h, GLint internalFormat, GLint scaling, GLint repeat); GLuint CreateTexture(const std::string& file, GLint internalFormat, GLint scaling, GLint repeat); float BlackmanWindow(float in, size_t i, size_t length); - void SmoothingOverTime(float* outputBuffer, float* lastOutputBuffer, kiss_fft_cpx* inputBuffer, size_t length, float smoothingTimeConstant, unsigned int fftSize); + void SmoothingOverTime(std::vector& outputBuffer, const std::vector& lastOutputBuffer, + kiss_fft_cpx* inputBuffer, size_t length, float smoothingTimeConstant, unsigned int fftSize); float LinearToDecibels(float linear); int DetermineBitsPrecision(); bool UpdateAlbumart(); void GatherDefines(); - //double MeasurePerformance(const std::string& shaderPath, int size); + // FFT configuration + static constexpr size_t AUDIO_BUFFER = 1024; + static constexpr size_t NUM_BANDS = AUDIO_BUFFER / 2; + + // Audio processing kiss_fft_cfg m_kissCfg; - GLubyte* m_audioData; - float* m_magnitudeBuffer; - float* m_pcm; + std::vector m_audioData; + std::vector m_magnitudeBuffer; + std::vector m_pcm; + // State bool m_initialized = false; int64_t m_initialTime = 0; // in ms double m_lastAlbumChange = 0; bool m_AlbumNeedsUpload = true; bool m_lowpower = false; - float m_albumX = 0.0; - float m_albumY = 0.0; + float m_albumX = 0.0f; + float m_albumY = 0.0f; int m_bitsPrecision = 0; int m_currentPreset = 0; float m_dotMode = false; - float m_dotSize = 0.0; - float m_fallSpeed = 0.25; - float m_distortThreshold = 0.0; - float m_noiseFluctuation = 0.0; - float m_rainHighlights = 0.0; + float m_dotSize = 0.0f; + float m_fallSpeed = 0.25f; + float m_distortThreshold = 0.0f; + float m_noiseFluctuation = 0.0f; + float m_rainHighlights = 0.0f; bool m_crtCurve = false; int m_samplesPerSec = 0; // Given by Start(...) bool m_needsUpload = true; // Set by AudioData(...) to mark presence of data - std::string m_albumArt = ""; - std::string m_defines = ""; + std::string m_albumArt; + std::string m_defines; + std::string m_usedShaderFile; - //GLint m_attrResolutionLoc = 0; + // OpenGL locations GLint m_attrGlobalTimeLoc = 0; GLint m_attrAlbumPositionLoc = 0; GLint m_attrAlbumRGBLoc = 0; - //GLint m_attrChannelTimeLoc = 0; - //GLint m_attrMouseLoc = 0; - //GLint m_attrDateLoc = 0; - //GLint m_attrSampleRateLoc = 0; - //GLint m_attrChannelResolutionLoc = 0; GLint m_attrChannelLoc[4] = {0}; GLuint m_channelTextures[4] = {0}; - //GLint m_attrDotSizeLoc = 0; kodi::gui::gl::CShaderProgram m_matrixShader; - //kodi::gui::gl::CShaderProgram m_displayShader; - struct + struct DotColor { - float red; - float green; - float blue; + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; } m_dotColor; - struct + struct State { - GLuint vertex_buffer; - GLuint attr_vertex_e; - GLuint attr_vertex_r, uTexture; - GLuint effect_fb; - GLuint framebuffer_texture; - GLuint uScale; - int fbwidth, fbheight; + GLuint vertex_buffer = 0; + GLuint attr_vertex_e = 0; + GLuint attr_vertex_r = 0; + GLuint uTexture = 0; + GLuint effect_fb = 0; + GLuint framebuffer_texture = 0; + GLuint uScale = 0; + int fbwidth = 0; + int fbheight = 0; } m_state; - std::string m_usedShaderFile; struct ShaderPath { bool audio = false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..3f84467 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.10) +project(visualization.matrix.tests) + +# Enable C++17 +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find GoogleTest +find_package(GTest REQUIRED) +find_package(GTestMain REQUIRED) + +# Include parent project for dependencies +include_directories( + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/lib/kissfft +) + +# Add test executable +add_executable(test_visualization_matrix + test_visualization_matrix.cpp +) + +# Link GoogleTest and required libraries +target_link_libraries(test_visualization_matrix + GTest::GTest + GTest::Main + m # For math functions (log10f, sin, cos, etc.) +) + +# Enable testing +enable_testing() + +# Add test to CTest +add_test(NAME test_visualization_matrix COMMAND test_visualization_matrix) diff --git a/tests/test_visualization_matrix.cpp b/tests/test_visualization_matrix.cpp new file mode 100644 index 0000000..f9bbd33 --- /dev/null +++ b/tests/test_visualization_matrix.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2024 Team Kodi + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSE.md for more information. + */ + +#include +#include +#include + +// Include the functions to test from main.cpp +// We need to extract the pure functions that don't depend on OpenGL/Kodi + +// --- Copied from main.cpp for testing --- + +#define AUDIO_BUFFER (1024) +#define NUM_BANDS (AUDIO_BUFFER / 2) + +struct kiss_fft_cpx +{ + float r; + float i; +}; + +// BlackmanWindow function (copied from main.cpp) +float BlackmanWindow(float in, size_t i, size_t length) +{ + double alpha = 0.16; + double a0 = 0.5 * (1.0 - alpha); + double a1 = 0.5; + double a2 = 0.5 * alpha; + + float x = (float)i / (float)length; + return in * (a0 - a1 * cos(2.0 * M_PI * x) + a2 * cos(4.0 * M_PI * x)); +} + +// LinearToDecibels function (copied from main.cpp) +float LinearToDecibels(float linear) +{ + if (!linear) + return -1000; + return 20 * log10f(linear); +} + +// SmoothingOverTime function (copied from main.cpp) +void SmoothingOverTime(float* outputBuffer, float* lastOutputBuffer, kiss_fft_cpx* inputBuffer, size_t length, float smoothingTimeConstant, unsigned int fftSize) +{ + for (size_t i = 0; i < length; i++) + { + kiss_fft_cpx c = inputBuffer[i]; + float magnitude = sqrt(c.r * c.r + c.i * c.i) / (float)fftSize; + outputBuffer[i] = smoothingTimeConstant * lastOutputBuffer[i] + (1.0 - smoothingTimeConstant) * magnitude; + } +} + +// --- Tests --- + +class VisualizationMatrixTest : public ::testing::Test +{ +protected: + void SetUp() override {} + void TearDown() override {} +}; + +// Test BlackmanWindow function +TEST_F(VisualizationMatrixTest, BlackmanWindow_ZeroInput) +{ + float result = BlackmanWindow(0.0f, 0, AUDIO_BUFFER); + EXPECT_FLOAT_EQ(result, 0.0f); +} + +TEST_F(VisualizationMatrixTest, BlackmanWindow_NonZeroInput) +{ + float result = BlackmanWindow(1.0f, 0, AUDIO_BUFFER); + // At position 0, the window should be: a0 - a1*cos(0) + a2*cos(0) = a0 - a1 + a2 + double alpha = 0.16; + double a0 = 0.5 * (1.0 - alpha); + double a1 = 0.5; + double a2 = 0.5 * alpha; + float expected = a0 - a1 + a2; + EXPECT_NEAR(result, expected, 0.0001f); +} + +TEST_F(VisualizationMatrixTest, BlackmanWindow_MiddlePosition) +{ + float result = BlackmanWindow(1.0f, AUDIO_BUFFER / 2, AUDIO_BUFFER); + // At middle position, x = 0.5 + float x = 0.5f; + double alpha = 0.16; + double a0 = 0.5 * (1.0 - alpha); + double a1 = 0.5; + double a2 = 0.5 * alpha; + float expected = a0 - a1 * cos(2.0 * M_PI * x) + a2 * cos(4.0 * M_PI * x); + EXPECT_NEAR(result, expected, 0.0001f); +} + +TEST_F(VisualizationMatrixTest, BlackmanWindow_EndPosition) +{ + float result = BlackmanWindow(1.0f, AUDIO_BUFFER - 1, AUDIO_BUFFER); + // At end position, x ~ 1.0 + float x = (float)(AUDIO_BUFFER - 1) / (float)AUDIO_BUFFER; + double alpha = 0.16; + double a0 = 0.5 * (1.0 - alpha); + double a1 = 0.5; + double a2 = 0.5 * alpha; + float expected = a0 - a1 * cos(2.0 * M_PI * x) + a2 * cos(4.0 * M_PI * x); + EXPECT_NEAR(result, expected, 0.0001f); +} + +// Test LinearToDecibels function +TEST_F(VisualizationMatrixTest, LinearToDecibels_Zero) +{ + float result = LinearToDecibels(0.0f); + EXPECT_FLOAT_EQ(result, -1000.0f); +} + +TEST_F(VisualizationMatrixTest, LinearToDecibels_One) +{ + float result = LinearToDecibels(1.0f); + EXPECT_FLOAT_EQ(result, 0.0f); // 20 * log10(1) = 0 +} + +TEST_F(VisualizationMatrixTest, LinearToDecibels_Ten) +{ + float result = LinearToDecibels(10.0f); + EXPECT_NEAR(result, 20.0f, 0.0001f); // 20 * log10(10) = 20 +} + +TEST_F(VisualizationMatrixTest, LinearToDecibels_Half) +{ + float result = LinearToDecibels(0.5f); + EXPECT_NEAR(result, -6.0206f, 0.0001f); // 20 * log10(0.5) ≈ -6.0206 +} + +TEST_F(VisualizationMatrixTest, LinearToDecibels_SmallValue) +{ + float result = LinearToDecibels(0.001f); + EXPECT_NEAR(result, -60.0f, 0.0001f); // 20 * log10(0.001) = -60 +} + +// Test SmoothingOverTime function +TEST_F(VisualizationMatrixTest, SmoothingOverTime_ZeroInput) +{ + std::vector outputBuffer(NUM_BANDS, 0.0f); + std::vector lastOutputBuffer(NUM_BANDS, 1.0f); + std::vector inputBuffer(NUM_BANDS); + + for (size_t i = 0; i < NUM_BANDS; i++) + { + inputBuffer[i].r = 0.0f; + inputBuffer[i].i = 0.0f; + } + + SmoothingOverTime(outputBuffer.data(), lastOutputBuffer.data(), inputBuffer.data(), NUM_BANDS, 0.5f, AUDIO_BUFFER); + + for (size_t i = 0; i < NUM_BANDS; i++) + { + // With zero input, output should be: 0.5 * lastOutputBuffer[i] + 0.5 * 0 = 0.5 * lastOutputBuffer[i] + EXPECT_NEAR(outputBuffer[i], 0.5f, 0.0001f); + } +} + +TEST_F(VisualizationMatrixTest, SmoothingOverTime_NonZeroInput) +{ + std::vector outputBuffer(NUM_BANDS, 0.0f); + std::vector lastOutputBuffer(NUM_BANDS, 0.0f); + std::vector inputBuffer(NUM_BANDS); + + // Set up input buffer with magnitude 1.0 for all bands + for (size_t i = 0; i < NUM_BANDS; i++) + { + inputBuffer[i].r = AUDIO_BUFFER; // magnitude * fftSize = 1.0 * 1024 + inputBuffer[i].i = 0.0f; + } + + SmoothingOverTime(outputBuffer.data(), lastOutputBuffer.data(), inputBuffer.data(), NUM_BANDS, 0.0f, AUDIO_BUFFER); + + for (size_t i = 0; i < NUM_BANDS; i++) + { + // With smoothingTimeConstant = 0, output should be: 0 * last + 1 * magnitude = magnitude + // magnitude = sqrt(r^2 + i^2) / fftSize = 1024 / 1024 = 1.0 + EXPECT_NEAR(outputBuffer[i], 1.0f, 0.0001f); + } +} + +TEST_F(VisualizationMatrixTest, SmoothingOverTime_FullSmoothing) +{ + std::vector outputBuffer(NUM_BANDS, 0.0f); + std::vector lastOutputBuffer(NUM_BANDS, 1.0f); + std::vector inputBuffer(NUM_BANDS); + + // Set up input buffer with magnitude 0.0 for all bands + for (size_t i = 0; i < NUM_BANDS; i++) + { + inputBuffer[i].r = 0.0f; + inputBuffer[i].i = 0.0f; + } + + // With smoothingTimeConstant = 1.0, output should be: 1.0 * lastOutputBuffer[i] + 0 * magnitude = lastOutputBuffer[i] + SmoothingOverTime(outputBuffer.data(), lastOutputBuffer.data(), inputBuffer.data(), NUM_BANDS, 1.0f, AUDIO_BUFFER); + + for (size_t i = 0; i < NUM_BANDS; i++) + { + EXPECT_NEAR(outputBuffer[i], 1.0f, 0.0001f); + } +} + +// Test edge cases +TEST_F(VisualizationMatrixTest, BlackmanWindow_NegativeInput) +{ + float result = BlackmanWindow(-1.0f, 0, AUDIO_BUFFER); + EXPECT_FLOAT_EQ(result, -BlackmanWindow(1.0f, 0, AUDIO_BUFFER)); +} + +TEST_F(VisualizationMatrixTest, LinearToDecibels_NegativeInput) +{ + // Negative linear values should still work (though physically meaningless) + float result = LinearToDecibels(-1.0f); + // log10 of negative number is NaN, but we handle it gracefully + // In practice, this should not happen with magnitude values + EXPECT_TRUE(std::isnan(result) || result == -1000.0f); +} diff --git a/visualization.matrix/resources/language/resource.language.en_gb/strings.po b/visualization.matrix/resources/language/resource.language.en_gb/strings.po index f3b5409..b574a2d 100644 --- a/visualization.matrix/resources/language/resource.language.en_gb/strings.po +++ b/visualization.matrix/resources/language/resource.language.en_gb/strings.po @@ -139,3 +139,7 @@ msgstr "" msgctxt "#30107" msgid "Clean rain with waveform envelope" msgstr "" + +msgctxt "#30109" +msgid "Odysseus" +msgstr "" diff --git a/visualization.matrix/resources/shaders/neon.frag.glsl b/visualization.matrix/resources/shaders/neon.frag.glsl new file mode 100644 index 0000000..cdd9147 --- /dev/null +++ b/visualization.matrix/resources/shaders/neon.frag.glsl @@ -0,0 +1,45 @@ +void main(void) +{ + // General stuff + vec2 uv = getUV(); + + // Neon grid background + vec2 grid = floor(uv * vec2(cColumns * 2.0, cColumns * 1.5)); + float gridPulse = sin(iTime * 0.5 + grid.x + grid.y * 1.3) * 0.2 + 0.8; + vec3 neonBase = vec3(0.1, 0.05, 0.2) * gridPulse; + + // Rain with neon glow + vec2 gv = floor(uv * cColumns); + float rnd = h11(gv.x) + 0.1; + float bw = 1.2 - fract((gv.y * 0.0024) + iTime * rnd * 1.5); + + // FFT-based brightness (audio reactivity) + float fft = texture(iChannel0, vec2((1.0 - abs(uv.x)) * 0.7, 0.0)).x; + fft -= abs(uv.x) * 0.25; + + // Amplify neon effect with audio + bw *= 1.0 + fft * 0.6 * cRainHighlights; + bw += bw * clamp(pow(fft * 1.5 * cRainHighlights, 2.0) - 8.0, 0.0, 1.0); + bw += bw * clamp(pow(fft * 1.2 * cRainHighlights, 3.0) - 15.0, 0.0, 0.8); + bw = min(bw, 2.5); + + // Neon color mapping (cyan/magenta) + vec3 neonColor = vec3( + sin(gv.x * 0.3 + iTime * 0.2) * 0.4 + 0.6, // Red + sin(gv.x * 0.3 + iTime * 0.2 + 2.0) * 0.4 + 0.6, // Green + sin(gv.x * 0.3 + iTime * 0.2 + 4.0) * 0.4 + 0.8 // Blue + ); + + // Combine with grid + vec3 col = mix(neonBase, neonColor, bw * 0.8); + + // Add glow effect + float glow = bw * 0.5; + col += vec3(0.2, 0.4, 0.8) * glow * (1.0 - length(fract(uv * cColumns) - 0.5)); + + // Vignette to focus the neon effect + float vignette = length(uv) * cVIGNETTEINTENSITY * 1.5; + col -= vignette * 0.5; + + FragColor = vec4(col, 1.0); +} diff --git a/visualization.matrix/resources/shaders/odysseus.frag.glsl b/visualization.matrix/resources/shaders/odysseus.frag.glsl new file mode 100644 index 0000000..373822f --- /dev/null +++ b/visualization.matrix/resources/shaders/odysseus.frag.glsl @@ -0,0 +1,130 @@ +// Odysseus Matrix Shader +// Combines Odysseus, Hermes, π, Helvetia, and Mythos² themes +// Dynamic, interactive matrix visualization with audio reactivity + +precision highp float; + +uniform vec2 iResolution; +uniform float iTime; +uniform sampler2D iChannel0; // Audio texture +uniform sampler2D iChannel1; // Noise texture (if available) + +// Hash function for pseudo-randomness (π-inspired) +float hash(vec2 p) { + float pi = 3.14159265359; + p = 50.0 * fract(p * pi); + return fract(p.x * p.y * (p.x + p.y)); +} + +// Swiss cross SDF (Helvetia) +float crossSDF(vec2 uv, float size) { + vec2 a = abs(uv); + float m = min(a.x, a.y); + return max(m - size/4.0, length(a) - size/2.0); +} + +// Odysseus' spiral path +vec2 odysseusPath(float t) { + float speed = 0.1; + return vec2( + cos(t * speed) * 0.4, + sin(t * speed) * 0.4 + ); +} + +// Hermes' wing pattern (abstract) +float hermesWings(vec2 uv, float t) { + float wing = sin(uv.x * 10.0 + t * 2.0) * cos(uv.y * 10.0 + t * 1.5); + return wing * 0.5 + 0.5; +} + +// π-based noise for matrix symbols +float piNoise(vec2 uv, float t) { + float pi = 3.14159265359; + float n = hash(uv + t * 0.1); + return fract(n * pi * 1000.0); +} + +// Symbol selection (0: Odysseus, 1: Hermes, 2: Helvetia, 3: π) +vec4 getSymbol(vec2 uv, float t, float audioLevel) { + // Divide screen into a grid + vec2 gridUV = fract(uv * 20.0); + float cellId = floor(uv.x * 20.0) + floor(uv.y * 20.0) * 20.0; + + // Use π-based noise to select symbols + float symbolType = fract(cellId * 0.1031 + t * 0.05 + audioLevel * 10.0); + + // Symbol colors + vec3 odysseusColor = vec3(0.0, 1.0, 0.0); // Green (ship) + vec3 hermesColor = vec3(1.0, 1.0, 0.0); // Yellow (wings) + vec3 helvetiaColor = vec3(1.0, 0.0, 0.0); // Red (cross) + vec3 piColor = vec3(1.0, 0.5, 0.0); // Orange (π) + + // Assign symbols based on noise + vec3 symbolColor; + if (symbolType < 0.25) { + symbolColor = odysseusColor; // ⛵ Odysseus + } else if (symbolType < 0.5) { + symbolColor = hermesColor; // ✈ Hermes + } else if (symbolType < 0.75) { + symbolColor = helvetiaColor; // ➕ Helvetia + } else { + symbolColor = piColor; // π + } + + // Falling effect (Matrix-style) + float fallPos = fract(t * 0.5 + cellId * 0.01 + audioLevel * 5.0); + float symbolAlpha = smoothstep(0.0, 0.1, fallPos) * (1.0 - smoothstep(0.9, 1.0, fallPos)); + + return vec4(symbolColor, symbolAlpha); +} + +// Mythos²: Recursive matrix layers +vec4 mythos2(vec2 uv, float t, float audioLevel) { + vec4 col = vec4(0.0); + + // Layer 1: Background matrix + col += getSymbol(uv, t, audioLevel) * 0.7; + + // Layer 2: Smaller nested matrix + col += getSymbol(uv * 2.0, t * 1.5, audioLevel * 0.5) * 0.3; + + return col; +} + +void main() { + vec2 uv = gl_FragCoord.xy / iResolution.xy; + vec2 p = uv * 2.0 - 1.0; // Center coordinates + + // Audio reactivity (sample from iChannel0) + float audioLevel = 0.0; + if (iChannel0 != sampler2D(vec2(0.0))) { + vec2 audioUV = vec2(fract(iTime * 0.1), 0.5); + audioLevel = texture2D(iChannel0, audioUV).r; + } + + // Base color (dark background) + vec4 col = vec4(0.05, 0.05, 0.1, 1.0); + + // Mythos²: Recursive matrix layers + col += mythos2(uv, iTime, audioLevel); + + // Odysseus' path (green spiral) + vec2 pathPos = odysseusPath(iTime); + float pathDist = distance(p, pathPos); + col.rgb += smoothstep(0.05, 0.0, pathDist) * vec3(0.0, 1.0, 0.0) * (0.5 + audioLevel * 0.5); + + // Hermes' wings (yellow dynamic shapes) + float wingPattern = hermesWings(uv, iTime); + col.rgb += wingPattern * vec3(1.0, 1.0, 0.0) * (0.3 + audioLevel * 0.2); + + // Helvetia's Swiss cross (red, centered) + float cross = crossSDF(p, 0.5); + col.rgb += smoothstep(0.05, 0.0, cross) * vec3(1.0, 0.0, 0.0) * (0.7 + audioLevel * 0.3); + + // π-based noise overlay + float noise = piNoise(uv, iTime); + col.rgb += noise * 0.1 * vec3(1.0, 0.5, 0.0); + + gl_FragColor = col; +}