diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 3ec70a9b..032344fd 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -76,7 +76,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: git fetch --prune --unshallow --tags @@ -364,7 +364,7 @@ jobs: - name: Archive artifacts (welle.io apk) if: always() && steps.build_apk.outcome == 'success' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io apk (for all android abi) path: build/android-build/build/outputs/apk/*/*.apk @@ -372,7 +372,7 @@ jobs: - name: Archive artifacts (welle.io apk built by qmake) if: always() && steps.build_apk_qmake.outcome == 'success' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io apk built by qmake (arm64-v8a only) path: qmake-build/src/welle-gui/android-build/build/outputs/apk/debug/android-build-debug.apk @@ -380,7 +380,7 @@ jobs: - name: Archive artifacts (welle.io build dir) if: always() && steps.build_apk.outcome == 'failure' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io build dir path: build/* @@ -388,7 +388,7 @@ jobs: - name: Archive artifacts (welle.io qmake-build dir) if: always() && steps.build_apk_qmake.outcome == 'failure' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io qmake-build dir path: qmake-build/* diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 391b86c5..ea0c7a14 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: git fetch --prune --unshallow --tags @@ -44,17 +44,20 @@ jobs: - name: Prepare flatpak build run: | - # Add current build date into appdata.xml + # Add shared modules + git submodule add https://github.com/flathub/shared-modules.git + + # Add current build date into metainfo.xml APPDATA_DATE=`date +%Y-%m-%d` - sed -i 's/date="1970-01-01"/'date=\""$APPDATA_DATE"\"'/g' io.welle.welle-gui.appdata.xml + sed -i 's/date="1970-01-01"/'date=\""$APPDATA_DATE"\"'/g' io.welle.welle-gui.metainfo.xml - # Add githash into appdata.xml + # Add githash into metainfo.xml #APPDATA_DATE_VERSION="2.7-unstable-$GIT_HASH" APPDATA_DATE_VERSION="2.7-$GIT_HASH" - sed -i 's/version="0.0.0"/'version=\""$APPDATA_DATE_VERSION"\"'/g' io.welle.welle-gui.appdata.xml + sed -i 's/version="0.0.0"/'version=\""$APPDATA_DATE_VERSION"\"'/g' io.welle.welle-gui.metainfo.xml - # Check if file "io.welle.welle-gui.appdata.xml" is valid - flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream io.welle.welle-gui.appdata.xml + # Check if file "io.welle.welle-gui.metainfo.xml" is valid + flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream io.welle.welle-gui.metainfo.xml - name: Build id: build @@ -77,7 +80,7 @@ jobs: # Prepare publish mkdir publish mv welle-io.flatpak publish/"$DATE"_"$GIT_HASH"_Linux_welle_"$ARCH_SUFFIX".flatpak - #cp io.welle.welle-gui.appdata.xml publish/ + #cp io.welle.welle-gui.metainfo.xml publish/ - name: Check flathub eligibility continue-on-error: true @@ -86,7 +89,7 @@ jobs: flatpak run --command=flatpak-builder-lint org.flatpak.Builder repo repo - name: Archive artifacts (welle.io Flatpak) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io Flatpak (${{ env.ARCH_SUFFIX }}) path: publish/* diff --git a/.github/workflows/raspberrypi.yml b/.github/workflows/raspberrypi.yml index aa31c8ee..cbc45efa 100644 --- a/.github/workflows/raspberrypi.yml +++ b/.github/workflows/raspberrypi.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: git fetch --prune --unshallow --tags @@ -40,7 +40,7 @@ jobs: - name: Archive artifacts (welle.io build dir) if: always() && steps.build.outcome == 'failure' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io build dir path: build/* diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 389678f9..7e32b0eb 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -16,13 +16,12 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: '6.8.1' + version: '6.10.1' modules: 'qtcharts qtmultimedia qt5compat qtshadertools' arch: 'win64_mingw' tools: 'tools_mingw1310' - aqtversion: '==3.1.19' # See https://github.com/jurplel/install-qt-action/issues/270 - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: git fetch --prune --unshallow --tags @@ -84,7 +83,7 @@ jobs: - name: Archive artifacts (welle.io Windows installer) id: upload_artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io Windows installer path: to_publish\*.exe @@ -92,7 +91,7 @@ jobs: - name: Sign installer with self-signed certificate with SignPath id: signing_installer - uses: signpath/github-action-submit-signing-request@v1 + uses: signpath/github-action-submit-signing-request@v2 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: 'b7a507e8-ab76-445f-bfb5-05944bcbbee9' @@ -104,7 +103,7 @@ jobs: - name: Archive artifacts (welle.io Windows installer signed) id: upload_artifact_self_signed - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: welle.io Windows installer self-signed path: publish\*.exe @@ -121,7 +120,7 @@ jobs: - name: Production sign installer with SignPath id: production_signing_installer - uses: signpath/github-action-submit-signing-request@v1 + uses: signpath/github-action-submit-signing-request@v2 # Production signing needs a manual step in SignPath within 5 minutes. # During development we don't need each build signed with a production key. @@ -139,7 +138,7 @@ jobs: - name: Archive artifacts (welle.io Windows installer signed) id: upload_artifact_production_signed - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() && steps.production_signing_installer.outcome == 'success' with: name: welle.io Windows installer production-signed diff --git a/.gitignore b/.gitignore index 3cb656ee..7949713d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,9 @@ welle.io.pro.user cmake_install.cmake cmake_uninstall.cmake install_manifest.txt + +# Android build artifacts +/build-android-arm64/ +/dist/ +/aqtinstall.log +/LOG.md diff --git a/BUILDING_ANDROID.md b/BUILDING_ANDROID.md new file mode 100644 index 00000000..3295e6d4 --- /dev/null +++ b/BUILDING_ANDROID.md @@ -0,0 +1,76 @@ +# Building welle.io for Android (Qt 6.5.3) + +This document describes the known-good setup to build the Android APK with Qt 6.5.3. + +## Requirements + +- Qt **6.5.3** +- Android SDK/NDK (tested with NDK r27) +- JDK 17 + +### Required Qt modules + +Install these Qt 6.5.3 modules for **both** `desktop` and `android_arm64_v8a`: + +- `qtcharts` +- `qtmultimedia` +- `qtconnectivity` +- `qtpositioning` +- `qtserialport` + +> Note: the Android build currently targets `arm64-v8a`. + +## Environment + +Set the following environment variables (examples use the local Qt install at `~/qt-cli/6.5.3`): + +```bash +export ANDROID_SDK_ROOT="$HOME/Android/Sdk" +export ANDROID_HOME="$HOME/Android/Sdk" + +export QT_ANDROID_PREFIX_ARM64="$HOME/qt-cli/6.5.3/android_arm64_v8a" +export QT_HOST_PATH="$HOME/qt-cli/6.5.3/gcc_64" +export QT_CMAKE_BIN="$QT_HOST_PATH/bin/qt-cmake" +``` + +## Install Qt modules (aqtinstall) + +If you used `aqtinstall` to install Qt: + +```bash +python3 -m venv ~/.venvs/aqt +~/.venvs/aqt/bin/pip install -U pip aqtinstall + +~/.venvs/aqt/bin/aqt install-qt linux desktop 6.5.3 gcc_64 \ + -m qtcharts qtmultimedia qtconnectivity qtpositioning qtserialport \ + --outputdir ~/qt-cli + +~/.venvs/aqt/bin/aqt install-qt linux android 6.5.3 android_arm64_v8a \ + -m qtcharts qtmultimedia qtconnectivity qtpositioning qtserialport \ + --outputdir ~/qt-cli +``` + +## Build steps + +From the repo root: + +```bash +QT_ANDROID_PREFIX_ARM64="$HOME/qt-cli/6.5.3/android_arm64_v8a" \ +QT_HOST_PATH="$HOME/qt-cli/6.5.3/gcc_64" \ +QT_CMAKE_BIN="$HOME/qt-cli/6.5.3/gcc_64/bin/qt-cmake" \ +./tools/android/build.sh +``` + +The APK is written to: + +``` +./dist/welle-io-arm64-v8a.apk +``` + +If a debug keystore exists at `~/.android/debug.keystore`, the script will sign the APK for local install convenience. + +## Install to device + +```bash +adb install -r dist/welle-io-arm64-v8a.apk +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e0ef762..4cf06d5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.2) +cmake_minimum_required(VERSION 3.16) project(Welle.Io LANGUAGES C CXX) @@ -17,6 +17,7 @@ set(CMAKE_CXX_STANDARD 14) option(BUILD_WELLE_IO "Build Welle.io" ON ) option(BUILD_WELLE_CLI "Build welle-cli" ON ) +option(LIBWELLE_STATIC "Build the welle library as static" ON ) option(WITH_APP_BUNDLE "Enable Application Bundle for macOS" ON ) option(KISS_FFT "KISS FFT instead of FFTW" OFF ) option(PROFILING "Enable profiling (see README.md)" OFF ) @@ -24,10 +25,10 @@ option(AIRSPY "Compile with Airspy support" OFF ) option(RTLSDR "Compile with RTL-SDR support" OFF ) option(SOAPYSDR "Compile with SoapySDR support" OFF ) option(FLAC "Compile with flac support for streaming" OFF ) +option(FDK_AAC "Use FDK-AAC instead of FAAD" OFF ) add_definitions(-Wall) add_definitions(-g) -add_definitions(-DDABLIN_AAC_FAAD2) if(MINGW) add_definitions(-municode) @@ -94,7 +95,21 @@ if(NOT ANDROID) set(fft_sources "") set(KISS_INCLUDE_DIRS "") endif() - find_package(Faad REQUIRED) + if (FDK_AAC) + find_package(FdkAac REQUIRED) + if (FDKAAC_FOUND) + add_definitions(-DDABLIN_AAC_FDKAAC) + set(AAC_LIBRARIES ${FDKAAC_LIBRARIES}) + set(AAC_INCLUDE_DIRS ${FDKAAC_INCLUDE_DIRS}) + endif() + else() + find_package(Faad REQUIRED) + if (FAAD_FOUND) + add_definitions(-DDABLIN_AAC_FAAD2) + set(AAC_LIBRARIES ${FAAD_LIBRARIES}) + set(AAC_INCLUDE_DIRS ${FAAD_INCLUDE_DIRS}) + endif() + endif() find_package(MPG123 REQUIRED) else() # For KISSFFT @@ -141,6 +156,7 @@ else() ) # For FAAD + add_definitions(-DDABLIN_AAC_FAAD2) add_definitions(-DHAVE_CONFIG_H) include_directories( src/libs/faad2 @@ -215,7 +231,7 @@ include_directories( src/libs/fec ${FFTW3F_INCLUDE_DIRS} ${KISS_INCLUDE_DIRS} - ${FAAD_INCLUDE_DIRS} + ${AAC_INCLUDE_DIRS} ${LIBRTLSDR_INCLUDE_DIRS} ${SoapySDR_INCLUDE_DIRS} ${FLACPP_INCLUDE_DIRS} @@ -345,14 +361,61 @@ add_definitions("-DCURRENT_VERSION=\"${CURRENT_VERSION}\"") STRING(TIMESTAMP BUILD_DATE "%s" UTC) add_definitions("-DBUILD_DATE=\"${BUILD_DATE}\"") +if (LIBWELLE_STATIC) + set(STATIC_OR_SHARED STATIC) +else () + set(STATIC_OR_SHARED SHARED) +endif() + +add_library(welle ${STATIC_OR_SHARED} + ${backend_sources} + ${faad_sources} + ${fft_sources} + ${input_sources} + ${mpg123_sources} +) + +string(REGEX MATCHALL + "([0-9]+)" + LIB_MATCH + "${CURRENT_VERSION}" +) + +list(POP_FRONT LIB_MATCH LIB_MATCH_1) +list(POP_FRONT LIB_MATCH LIB_MATCH_2) +if (NOT LIB_MATCH STREQUAL "") + list(POP_FRONT LIB_MATCH LIB_MATCH_3) +else () + set(LIB_MATCH_3 0) +endif () + +set_property(TARGET welle PROPERTY VERSION "${LIB_MATCH_1}.${LIB_MATCH_2}.${LIB_MATCH_3}") +set_property(TARGET welle PROPERTY SOVERSION "${LIB_MATCH_1}") +set_property(TARGET welle PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_link_libraries(welle PRIVATE + ${LIBRTLSDR_LIBRARIES} + ${LIBAIRSPY_LIBRARIES} + ${FFTW3F_LIBRARIES} + ${AAC_LIBRARIES} + ${SoapySDR_LIBRARIES} + ${MPG123_LIBRARIES} +) + +if (NOT LIBWELLE_STATIC) +install(TARGETS welle + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) +endif() + if(BUILD_WELLE_IO) set(executableName welle-io) if(CMAKE_AUTORCC) - qt_add_executable (${executableName} MANUAL_FINALIZATION ${welle_io_sources} ${backend_sources} ${input_sources} ${fft_sources} ${mpg123_sources} ${faad_sources} ${EXTRA_MOCS} src/welle-gui/resources.qrc) + qt_add_executable (${executableName} MANUAL_FINALIZATION ${welle_io_sources} ${EXTRA_MOCS} src/welle-gui/resources.qrc) else() qt_add_resources(welle_io_sources src/welle-gui/resources.qrc) - qt_add_executable (${executableName} MANUAL_FINALIZATION ${welle_io_sources} ${backend_sources} ${input_sources} ${fft_sources} ${mpg123_sources} ${faad_sources} ${EXTRA_MOCS}) + qt_add_executable (${executableName} MANUAL_FINALIZATION ${welle_io_sources} ${EXTRA_MOCS}) endif() if(ANDROID) @@ -401,10 +464,13 @@ if(BUILD_WELLE_IO) ) endif() if(Qt6Core_VERSION_MAJOR EQUAL 6 AND Qt6Core_VERSION_MINOR GREATER_EQUAL 3) - find_package(Qt6 COMPONENTS Quick3DUtils REQUIRED) - target_link_libraries (${executableName} PRIVATE - Qt6::Quick3DUtils - ) + # Quick3DUtils is optional on Android; only link if available. + find_package(Qt6 COMPONENTS Quick3DUtils) + if(Qt6Quick3DUtils_FOUND) + target_link_libraries (${executableName} PRIVATE + Qt6::Quick3DUtils + ) + endif() endif() else() # Qt6::DBus should not be used for Android @@ -413,12 +479,7 @@ if(BUILD_WELLE_IO) endif() target_link_libraries (${executableName} PRIVATE - ${LIBRTLSDR_LIBRARIES} - ${LIBAIRSPY_LIBRARIES} - ${FFTW3F_LIBRARIES} - ${FAAD_LIBRARIES} - ${SoapySDR_LIBRARIES} - ${MPG123_LIBRARIES} + welle Threads::Threads Qt6::Core Qt6::Widgets Qt6::Multimedia Qt6::Charts Qt6::Qml Qt6::Quick Qt6::QuickControls2 ) @@ -445,7 +506,7 @@ if(BUILD_WELLE_IO) if(UNIX AND NOT APPLE) INSTALL (FILES ${PROJECT_SOURCE_DIR}/io.welle.welle-gui.desktop DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) - INSTALL (FILES ${PROJECT_SOURCE_DIR}/io.welle.welle-gui.appdata.xml DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo) + INSTALL (FILES ${PROJECT_SOURCE_DIR}/io.welle.welle-gui.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo) INSTALL (FILES ${PROJECT_SOURCE_DIR}/src/welle-gui/icons/16x16/welle-io.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps RENAME io.welle.welle-gui.png) INSTALL (FILES ${PROJECT_SOURCE_DIR}/src/welle-gui/icons/24x24/welle-io.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/24x24/apps RENAME io.welle.welle-gui.png) @@ -461,9 +522,6 @@ if(BUILD_WELLE_CLI AND NOT ANDROID) set(cliExecutableName welle-cli) add_executable (${cliExecutableName} ${welle_cli_sources} - ${backend_sources} - ${input_sources} - ${fft_sources} index.html.h index.js.h favicon.ico.h) @@ -474,14 +532,9 @@ if(BUILD_WELLE_CLI AND NOT ANDROID) endif(CMAKE_BUILD_TYPE MATCHES Debug) target_link_libraries (${cliExecutableName} - ${LIBRTLSDR_LIBRARIES} - ${LIBAIRSPY_LIBRARIES} - ${FFTW3F_LIBRARIES} - ${FAAD_LIBRARIES} + welle ${ALSA_LIBRARIES} ${LAME_LIBRARIES} - ${SoapySDR_LIBRARIES} - ${MPG123_LIBRARIES} ${FLACPP_LIBRARIES} Threads::Threads ) diff --git a/LOG.md b/LOG.md new file mode 100644 index 00000000..a42fcaa3 --- /dev/null +++ b/LOG.md @@ -0,0 +1,183 @@ +# Android APK build + install log + +Date: 2026-02-22 +Host: Pop!_OS (per user), device connected via ADB + +## 1) SDK/ADB validation +Commands: +- `printenv ANDROID_SDK_ROOT ANDROID_HOME` +- `which adb sdkmanager apksigner aapt` +- `adb devices` + +Results: +- `ANDROID_SDK_ROOT=/home/leonamramosfoli/Android/Sdk` +- `ANDROID_HOME=/home/leonamramosfoli/Android/Sdk` +- `adb=/home/leonamramosfoli/Android/Sdk/platform-tools/adb` +- `sdkmanager=/home/leonamramosfoli/Android/Sdk/cmdline-tools/latest/bin/sdkmanager` +- `apksigner=/home/leonamramosfoli/Android/Sdk/build-tools/34.0.0/apksigner` +- `aapt=/home/leonamramosfoli/Android/Sdk/build-tools/34.0.0/aapt` +- `adb devices`: + - `adb-644875bb-aa7GxT._adb-tls-connect._tcp device` + +## 2) SDK packages present +Command: +- `sdkmanager --list | sed -n '1,120p'` + +Installed (local): +- build-tools: 34.0.0, 35.0.0, 36.1.0 +- platform-tools 36.0.2 +- platforms;android-34 +- ndk;26.1.10909125, ndk;26.3.11579264, ndk;27.0.12077973 + +## 3) Attempt to install missing packages +Command: +- `yes | sdkmanager "platforms;android-33" "ndk;25.1.8937393"` + +Result: +- Failed due to inability to fetch remote manifests (`Failed to download any source lists`). +- `platforms;android-33` not found (offline). + +## 4) Qt for Android availability +Checks: +- `command -v qt-cmake` → not found in PATH +- `/usr/lib/qt6/bin/qt-cmake` exists (host Qt tools only) +- No Qt Android SDK/ABI trees found under `/opt`, `/usr/lib/qt6`, or `$HOME`. + +## 5) Attempt to install Qt for Android (aqtinstall) +Commands: +- `python3 -m venv ~/.venvs/aqt && ~/.venvs/aqt/bin/pip install -U pip aqtinstall` + +Result: +- Failed to reach PyPI (`No address associated with hostname`), so `aqtinstall` not installable offline. + +## Current blocker +Building an Android APK requires a Qt for Android ABI installation (e.g. `/opt/Qt/6.5.x/android_arm64_v8a`) which is not present locally and cannot be downloaded without network access to Qt or PyPI. + +## Build script created +- `tools/android/build.sh` + - Reproducible, headless build for `arm64-v8a` + - Produces `dist/welle-io-arm64-v8a.apk` + - Requires `QT_ANDROID_PREFIX_ARM64` to be set to an existing Qt Android arm64 prefix. + +## Next steps (pending unblock) +1) Provide a local Qt for Android installation path (arm64-v8a), or allow network access to install it. +2) Run `tools/android/build.sh` to generate APK. +3) Validate APK with `apksigner` / `aapt` and install via `adb` to reproduce issue #814. +4) Capture `adb install` error and `adb logcat`, then apply minimal fix. + +## 6) Build attempt with local Qt Android +Command: +- `QT_ANDROID_PREFIX_ARM64=/home/leonamramosfoli/Qt/6.5.3/android_arm64_v8a \ + QT_CMAKE_BIN=/home/leonamramosfoli/Qt/6.5.3/gcc_64/bin/qt-cmake \ + QT_HOST_PATH=/home/leonamramosfoli/Qt/6.5.3/gcc_64 \ + tools/android/build.sh` + +Result: +- CMake failed: Qt6Multimedia not found in host Qt. +- Missing config: + - `/home/leonamramosfoli/Qt/6.5.3/gcc_64/lib/cmake/Qt6Multimedia/Qt6MultimediaConfig.cmake` + +Verification: +- `ls /home/leonamramosfoli/Qt/6.5.3/gcc_64/lib/cmake | grep Multimedia` → no results +- `ls /home/leonamramosfoli/Qt/6.5.3/android_arm64_v8a/lib/cmake | grep Multimedia` → no results + +Blocker: +- Qt6 Multimedia module is missing for both host and Android arm64 Qt installs. +- Build cannot proceed without installing Qt Multimedia for 6.5.3 (host + android_arm64_v8a). + +## 7) Auto-install missing Qt module via aqtinstall (attempt) +Detected missing module: Qt6Multimedia + +Action: +- `tools/android/install-qt-module.sh Multimedia` + +Result: +- Failed because `pip` cannot reach PyPI (no network). +- `ERROR: Could not find a version that satisfies the requirement aqtinstall` + +Consequence: +- Automated module install via `aqtinstall` cannot proceed offline. + +## 8) Build attempt with qt-cli Qt (clean build dir) +Command: +- `rm -rf build-android-arm64` +- `QT_ANDROID_PREFIX_ARM64=/home/leonamramosfoli/qt-cli/6.5.3/android_arm64_v8a \ + QT_CMAKE_BIN=/home/leonamramosfoli/qt-cli/6.5.3/gcc_64/bin/qt-cmake \ + QT_HOST_PATH=/home/leonamramosfoli/qt-cli/6.5.3/gcc_64 \ + tools/android/build.sh` + +Result: +- CMake failed: Qt6Charts not found in host Qt. +- Missing config: + - `/home/leonamramosfoli/qt-cli/6.5.3/gcc_64/lib/cmake/Qt6Charts/Qt6ChartsConfig.cmake` + +Blocker: +- Qt6 Charts module missing in qt-cli install (host + likely android). Build cannot continue. + +## 9) Build failure: missing Lame +Error: +- `Could NOT find Lame (missing: LAME_INCLUDE_DIRS LAME_LIBRARIES)` + +Attempted fix: +- `sudo apt install -y libmp3lame-dev` + +Result: +- sudo requires password (TTY). Cannot proceed automatically in this environment. + +## 10) Install Qt modules via aqtinstall (online) +Commands: +- `python3 -m venv ~/.venvs/aqt && ~/.venvs/aqt/bin/pip install -U pip aqtinstall` +- `~/.venvs/aqt/bin/aqt install-qt linux desktop 6.5.3 gcc_64 -m qtcharts qtmultimedia --outputdir ~/Qt` +- `~/.venvs/aqt/bin/aqt install-qt linux android 6.5.3 android_arm64_v8a -m qtcharts qtmultimedia --outputdir ~/Qt` + +Result: +- Qt Charts + Multimedia installed for host and Android arm64. + +## 11) Build APK (arm64-v8a) +Command: +- `QT_ANDROID_PREFIX_ARM64=/home/leonamramosfoli/Qt/6.5.3/android_arm64_v8a \ + QT_CMAKE_BIN=/home/leonamramosfoli/Qt/6.5.3/gcc_64/bin/qt-cmake \ + QT_HOST_PATH=/home/leonamramosfoli/Qt/6.5.3/gcc_64 \ + tools/android/build.sh` + +Result: +- Build succeeded. +- APK created: `dist/welle-io-arm64-v8a.apk` (unsigned) + +Notes: +- CMake used cached Qt paths under `/home/leonamramosfoli/qt-cli/...` in the build dir. + +## 12) APK validation (unsigned) +Commands: +- `apksigner verify --verbose --print-certs dist/welle-io-arm64-v8a.apk` +- `aapt dump badging dist/welle-io-arm64-v8a.apk` + +Results: +- `apksigner`: `DOES NOT VERIFY` / `ERROR: Missing META-INF/MANIFEST.MF` +- `aapt` shows `native-code: 'arm64-v8a'` and targetSdkVersion 31. + +## 13) Install attempt (unsigned) +Command: +- `adb install -r dist/welle-io-arm64-v8a.apk` + +Result: +- Failure: `INSTALL_PARSE_FAILED_NO_CERTIFICATES` (matches issue #814 symptoms) + +## 14) Sign APK with debug keystore +Command: +- `apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey --out dist/welle-io-arm64-v8a-signed.apk dist/welle-io-arm64-v8a.apk` + +Result: +- Signed APK verifies with v1/v2/v3 schemes. + +## 15) Install signed APK +Command: +- `adb install -r dist/welle-io-arm64-v8a-signed.apk` + +Result: +- Success (after a harmless "incremental install not allowed" warning). + +## 16) Fix applied in build script +Change: +- `tools/android/build.sh` now copies unsigned APK to `dist/welle-io-arm64-v8a-unsigned.apk` and, if debug keystore is present, signs to `dist/welle-io-arm64-v8a.apk` automatically. +- Also fixed `rg --no-daemon` usage to avoid ripgrep flag parsing errors. diff --git a/cmake/Modules/FindFaad.cmake b/cmake/Modules/FindFaad.cmake index 541861a7..54ad8f20 100644 --- a/cmake/Modules/FindFaad.cmake +++ b/cmake/Modules/FindFaad.cmake @@ -23,12 +23,7 @@ mark_as_advanced(FAAD_LIBRARY) mark_as_advanced(FAAD_FOUND) if(NOT FAAD_FOUND) - set(FAAD_DIR_MESSAGE "libfaad was not found. Make sure FAAD_LIBRARY and FAAD_INCLUDE_DIR are set.") - if(NOT FAAD_FIND_QUIETLY) - message(STATUS "${FAAD_DIR_MESSAGE}") - else(NOT FAAD_FIND_QUIETLY) - if(FAAD_FIND_REQUIRED) - message(FATAL_ERROR "${FAAD_DIR_MESSAGE}") - endif(FAAD_FIND_REQUIRED) - endif(NOT FAAD_FIND_QUIETLY) + message(FATAL_ERROR "libfaad was not found. Make sure FAAD_LIBRARY and FAAD_INCLUDE_DIR are set.") +else(NOT FAAD_FOUND) + message(STATUS "Found libfaad: ${FAAD_INCLUDE_DIR}, ${FAAD_LIBRARY}") endif(NOT FAAD_FOUND) diff --git a/cmake/Modules/FindFdkAac.cmake b/cmake/Modules/FindFdkAac.cmake new file mode 100644 index 00000000..ce2b69ad --- /dev/null +++ b/cmake/Modules/FindFdkAac.cmake @@ -0,0 +1,29 @@ +# Try to find FDKAAC library and include path. +# Once done this will define +# +# FDKAAC_INCLUDE_DIRS - where to find faad.h, etc. +# FDKAAC_LIBRARIES - List of libraries when using libfaad. +# FDKAAC_FOUND - True if libfaad found. + +find_path(FDKAAC_INCLUDE_DIR fdk-aac/aacdecoder_lib.h DOC "The directory where fdk-aac/aacdecoder_lib.h resides") +find_library(FDKAAC_LIBRARY NAMES fdk-aac DOC "The libfdk-aac library") + +if(FDKAAC_INCLUDE_DIR AND FDKAAC_LIBRARY) + set(FDKAAC_FOUND 1) + set(FDKAAC_LIBRARIES ${FDKAAC_LIBRARY}) + set(FDKAAC_INCLUDE_DIRS ${FDKAAC_INCLUDE_DIR}) +else(FDKAAC_INCLUDE_DIR AND FDKAAC_LIBRARY) + set(FDKAAC_FOUND 0) + set(FDKAAC_LIBRARIES) + set(FDKAAC_INCLUDE_DIRS) +endif(FDKAAC_INCLUDE_DIR AND FDKAAC_LIBRARY) + +mark_as_advanced(FDKAAC_INCLUDE_DIR) +mark_as_advanced(FDKAAC_LIBRARY) +mark_as_advanced(FDKAAC_FOUND) + +if(NOT FDKAAC_FOUND) + message(FATAL_ERROR "fdk-aac was not found. Make sure FDKAAC_LIBRARY and FDKAAC_INCLUDE_DIR are set.") +else(NOT FDKAAC_FOUND) + message(STATUS "Found fdk-aac: ${FDKAAC_INCLUDE_DIR}, ${FDKAAC_LIBRARY}") +endif(NOT FDKAAC_FOUND) diff --git a/io.welle.welle-gui.appdata.xml b/io.welle.welle-gui.metainfo.xml similarity index 100% rename from io.welle.welle-gui.appdata.xml rename to io.welle.welle-gui.metainfo.xml diff --git a/io.welle.welle-gui.yml b/io.welle.welle-gui.yml index 4afe5be2..dca7a920 100644 --- a/io.welle.welle-gui.yml +++ b/io.welle.welle-gui.yml @@ -1,6 +1,6 @@ app-id: io.welle.welle-gui runtime: org.kde.Platform -runtime-version: '6.8' +runtime-version: '6.10' sdk: org.kde.Sdk command: welle-io finish-args: @@ -14,20 +14,11 @@ finish-args: - '--device=all' modules: - - name: fftw - buildsystem: autotools - config-opts: - - '--enable-shared' - - '--disable-static' - - '--enable-threads' - - '--enable-float' - sources: - - type: archive - url: 'http://fftw.org/fftw-3.3.10.tar.gz' - md5: 8ccbf6a5ea78a16dbc3e1306e234cc5c + - shared-modules/linux-audio/fftw3f.json + - shared-modules/libusb/libusb.json - name: libfaad - buildsystem: cmake + buildsystem: cmake-ninja config-opts: - '-DCMAKE_INSTALL_LIBDIR=lib' sources: @@ -35,53 +26,80 @@ modules: url: 'https://salsa.debian.org/multimedia-team/faad2.git' tag: debian/2.11.2-1 - - name: libusb - buildsystem: autotools - config-opts: - - '--disable-udev' - sources: - - type: git - url: 'https://github.com/libusb/libusb.git' - tag: v1.0.27 - - name: librtlsdr - buildsystem: cmake + buildsystem: cmake-ninja config-opts: - - '-Wno-dev' - - '-DDETACH_KERNEL_DRIVER=ON' - '-DCMAKE_INSTALL_LIBDIR=lib' + - '-DDETACH_KERNEL_DRIVER=ON' + - '-Wno-dev' sources: - type: git url: 'https://gitea.osmocom.org/sdr/rtl-sdr.git' tag: v2.0.2 - name: libairspy - buildsystem: cmake + buildsystem: cmake-ninja + build-options: + cflags: "-std=gnu17" config-opts: + - '-DCMAKE_POLICY_VERSION_MINIMUM=3.5' - '-Wno-dev' sources: - type: git url: 'https://github.com/airspy/airspyone_host.git' tag: v1.0.10 + - name: libhackrf + buildsystem: cmake-ninja + subdir: host + config-opts: + - '-DINSTALL_UDEV_RULE=OFF' + - '-Wno-dev' + sources: + - type: git + url: 'https://github.com/greatscottgadgets/hackrf.git' + tag: v2026.01.3 + - name: SoapySDR - buildsystem: cmake + buildsystem: cmake-ninja config-opts: - '-DCMAKE_BUILD_TYPE=Release' - - '-Wno-dev' - '-DCMAKE_INSTALL_LIBDIR=lib' + - '-DCMAKE_POLICY_VERSION_MINIMUM=3.5' + - '-Wno-dev' sources: - type: git url: 'https://github.com/pothosware/SoapySDR.git' tag: soapy-sdr-0.8.1 + - name: SoapyHackRF + buildsystem: cmake-ninja + config-opts: + - '-DCMAKE_INSTALL_LIBDIR=lib' + - '-Wno-dev' + sources: + - type: git + url: 'https://github.com/pothosware/SoapyHackRF.git' + commit: 143ff5e7e0f786e341df8846c04e8273c5183c26 + + - name: SoapyRemote + buildsystem: cmake-ninja + config-opts: + - '-DCMAKE_INSTALL_LIBDIR=lib' + - '-Wno-dev' + sources: + - type: git + url: 'https://github.com/pothosware/SoapyRemote.git' + commit: 40c3ef9053b63885b7444ce7e9ef00d2c7964c9d + - name: welle.io buildsystem: cmake-ninja config-opts: - - -DBUILD_WELLE_CLI=OFF - - -DAIRSPY=ON - - -DRTLSDR=ON - - -DSOAPYSDR=ON + - '-DBUILD_WELLE_CLI=OFF' + - '-DCMAKE_POLICY_VERSION_MINIMUM=3.5' + - '-DAIRSPY=ON' + - '-DRTLSDR=ON' + - '-DSOAPYSDR=ON' sources: - type: dir - path: . \ No newline at end of file + path: . diff --git a/src/backend/subchannel_sink.h b/src/backend/subchannel_sink.h index 826c9d22..4d234430 100644 --- a/src/backend/subchannel_sink.h +++ b/src/backend/subchannel_sink.h @@ -52,6 +52,7 @@ class SubchannelSinkObserver { virtual void ProcessPAD(const uint8_t* /*xpad_data*/, size_t /*xpad_len*/, bool /*exact_xpad_len*/, const uint8_t* /*fpad_data*/) {} virtual void AudioError(const std::string& /*hint*/) {} + virtual void AudioWarning(const std::string& /*hint*/) {} virtual void ACCFrameError(const unsigned char /* error*/) {} virtual void FECInfo(int /*total_corr_count*/, bool /*uncorr_errors*/) {} }; diff --git a/src/welle-cli/alsa-output.cpp b/src/welle-cli/alsa-output.cpp index 6c570f15..392c0d9a 100644 --- a/src/welle-cli/alsa-output.cpp +++ b/src/welle-cli/alsa-output.cpp @@ -29,12 +29,14 @@ #include "welle-cli/alsa-output.h" using namespace std; -#define PCM_DEVICE "default" AlsaOutput::AlsaOutput(int chans, unsigned int rate) : + AlsaOutput(PCM_DEVICE, chans, rate) {} + +AlsaOutput::AlsaOutput(const char* device, int chans, unsigned int rate) : channels(chans) { - int err = snd_pcm_open(&pcm_handle, PCM_DEVICE, SND_PCM_STREAM_PLAYBACK, 0); + int err = snd_pcm_open(&pcm_handle, device, SND_PCM_STREAM_PLAYBACK, 0); if (err < 0) { fprintf(stderr, "ERROR: Can't open \"%s\" PCM device. %s\n", PCM_DEVICE, snd_strerror(err)); @@ -116,13 +118,10 @@ void AlsaOutput::playPCM(std::vector&& pcm) snd_pcm_sframes_t ret = snd_pcm_writei(pcm_handle, data, frames_to_send); - if (ret == -EPIPE) { - snd_pcm_prepare(pcm_handle); - fprintf(stderr, "XRUN\n"); - this_thread::sleep_for(chrono::milliseconds(20)); - break; + if (ret < 0) { + ret = snd_pcm_recover(pcm_handle, ret, 0); } - else if (ret < 0) { + if (ret < 0) { fprintf(stderr, "ERROR: Can't write to PCM device. %s\n", snd_strerror(ret)); break; diff --git a/src/welle-cli/alsa-output.h b/src/welle-cli/alsa-output.h index 8a410e27..bf7a9ac0 100644 --- a/src/welle-cli/alsa-output.h +++ b/src/welle-cli/alsa-output.h @@ -31,6 +31,7 @@ class AlsaOutput { public: + AlsaOutput(const char* device, int chans, unsigned int rate); AlsaOutput(int chans, unsigned int rate); ~AlsaOutput(); AlsaOutput(const AlsaOutput& other) = delete; diff --git a/src/welle-cli/webradiointerface.cpp b/src/welle-cli/webradiointerface.cpp index 3582fa47..452e1a56 100644 --- a/src/welle-cli/webradiointerface.cpp +++ b/src/welle-cli/webradiointerface.cpp @@ -363,12 +363,18 @@ static vector recv_exactly(Socket& s, size_t num_bytes) return buf; } -static vector split(const string& str, char c = ' ') +static vector split(const string& str, char c = ' ', size_t max_splits = string::npos) { const char *s = str.data(); vector result; do { const char *begin = s; + + if (result.size() >= max_splits) { + result.push_back(s); + break; + } + while (*s != c && *s) s++; result.push_back(string(begin, s)); @@ -386,6 +392,26 @@ struct http_request_t { string post_data; }; +static string trim(const string& str) { + auto start = str.begin(); + while (start != str.end() && isspace(*start)) ++start; + auto end = str.end(); + do { --end; } while (end != start && isspace(*end)); + return string(start, end + 1); +} + +static string get_base_uri(const map& headers) { + if (headers.find("Referer") != headers.end()) { + string referer = headers.at("Referer"); + if (referer.back() != '/') + referer += '/'; + return referer; + } else if (headers.find("Host") != headers.end()) { + return "http://" + headers.at("Host") + "/"; + } else { + return "/"; + } +} static http_request_t parse_http_headers(Socket& s) { http_request_t r; @@ -416,10 +442,10 @@ static http_request_t parse_http_headers(Socket& s) { break; } - const auto header = split(header_line, ':'); + const auto header = split(header_line, ':', 1); if (header.size() == 2) { - r.headers.emplace(header[0], header[1]); + r.headers.emplace(header[0], trim(header[1])); } } @@ -482,7 +508,7 @@ bool WebRadioInterface::dispatch_client(Socket&& client) success = send_mux_json(s); } else if (req.url == "/mux.m3u") { - success = send_mux_playlist(s); + success = send_mux_playlist(s, get_base_uri(req.headers)); } else if (req.url == "/fic") { success = send_fic(s); @@ -818,7 +844,7 @@ bool WebRadioInterface::send_mux_json(Socket& s) return true; } -bool WebRadioInterface::send_mux_playlist(Socket& s) +bool WebRadioInterface::send_mux_playlist(Socket& s, string base_uri) { stringstream m3u; m3u << "#EXTM3U\n"; @@ -837,7 +863,7 @@ bool WebRadioInterface::send_mux_playlist(Socket& s) case TransportMode::Audio: if (sc.audioType() == AudioServiceComponentType::DAB or sc.audioType() == AudioServiceComponentType::DABPlus) { - url_mp3 = "/mp3/" + hex_sid; + url_mp3 = base_uri + "mp3/" + hex_sid; } break; default: diff --git a/src/welle-cli/webradiointerface.h b/src/welle-cli/webradiointerface.h index 936b8934..fc80e9b5 100644 --- a/src/welle-cli/webradiointerface.h +++ b/src/welle-cli/webradiointerface.h @@ -114,7 +114,7 @@ class WebRadioInterface : public RadioControllerInterface { bool send_mux_json(Socket& s); // Generate and send a m3u playlist with all services - bool send_mux_playlist(Socket& s); + bool send_mux_playlist(Socket& s, std::string base_uri); // Send a stream containing the selected programme. // stream is a service id, either in hex with 0x prefix or diff --git a/src/welle-cli/welle-cli.cpp b/src/welle-cli/welle-cli.cpp index 538c2f2c..b3548b68 100644 --- a/src/welle-cli/welle-cli.cpp +++ b/src/welle-cli/welle-cli.cpp @@ -32,14 +32,15 @@ #include #include +#include #include #include +#include #include #include #include #include #include -#include #include #ifdef HAVE_SOAPYSDR # include "soapy_sdr.h" @@ -72,8 +73,15 @@ using namespace nlohmann; #if defined(HAVE_ALSA) class AlsaProgrammeHandler: public ProgrammeHandlerInterface { public: + AlsaProgrammeHandler(const string& device) + { + if (!device.empty()) + { + pcm_device = device; + } + } virtual void onFrameErrors(int frameErrors) override { (void)frameErrors; } - virtual void onNewAudio(std::vector&& audioData, int sampleRate, const std::string& mode) override + virtual void onNewAudio(vector&& audioData, int sampleRate, const string& mode) override { (void)mode; lock_guard lock(aomutex); @@ -83,7 +91,7 @@ class AlsaProgrammeHandler: public ProgrammeHandlerInterface { if (!ao or reset_ao) { cerr << "Create audio output rate " << rate << endl; - ao = make_unique(2, rate); + ao = make_unique(pcm_device.c_str(), 2, rate); } ao->playPCM(move(audioData)); @@ -92,7 +100,7 @@ class AlsaProgrammeHandler: public ProgrammeHandlerInterface { virtual void onRsErrors(bool uncorrectedErrors, int numCorrectedErrors) override { (void)uncorrectedErrors; (void)numCorrectedErrors; } virtual void onAacErrors(int aacErrors) override { (void)aacErrors; } - virtual void onNewDynamicLabel(const std::string& label) override + virtual void onNewDynamicLabel(const string& label) override { cout << "DLS: " << label << endl; } @@ -108,12 +116,13 @@ class AlsaProgrammeHandler: public ProgrammeHandlerInterface { unique_ptr ao; bool stereo = true; unsigned int rate = 48000; + string pcm_device; }; #endif // defined(HAVE_ALSA) class WavProgrammeHandler: public ProgrammeHandlerInterface { public: - WavProgrammeHandler(uint32_t SId, const std::string& fileprefix) : + WavProgrammeHandler(uint32_t SId, const string& fileprefix) : SId(SId), filePrefix(fileprefix) {} ~WavProgrammeHandler() { @@ -127,10 +136,10 @@ class WavProgrammeHandler: public ProgrammeHandlerInterface { WavProgrammeHandler& operator=(WavProgrammeHandler&& other) = default; virtual void onFrameErrors(int frameErrors) override { (void)frameErrors; } - virtual void onNewAudio(std::vector&& audioData, int sampleRate, const string& mode) override + virtual void onNewAudio(vector&& audioData, int sampleRate, const string& mode) override { if (rate != sampleRate ) { - cout << "[0x" << std::hex << SId << std::dec << "] " << + cout << "[0x" << hex << SId << dec << "] " << "rate " << sampleRate << " mode " << mode << endl; string filename = filePrefix + ".wav"; @@ -153,9 +162,9 @@ class WavProgrammeHandler: public ProgrammeHandlerInterface { virtual void onRsErrors(bool uncorrectedErrors, int numCorrectedErrors) override { (void)uncorrectedErrors; (void)numCorrectedErrors; } virtual void onAacErrors(int aacErrors) override { (void)aacErrors; } - virtual void onNewDynamicLabel(const std::string& label) override + virtual void onNewDynamicLabel(const string& label) override { - cout << "[0x" << std::hex << SId << std::dec << "] " << + cout << "[0x" << hex << SId << dec << "] " << "DLS: " << label << endl; } @@ -233,12 +242,12 @@ class RadioInterface : public RadioControllerInterface { fwrite(buf.data(), buf.size(), sizeof(buf[0]), fic_fd); } } - virtual void onNewImpulseResponse(std::vector&& data) override { (void)data; } - virtual void onNewNullSymbol(std::vector&& data) override { (void)data; } - virtual void onConstellationPoints(std::vector&& data) override { (void)data; } - virtual void onMessage(message_level_t level, const std::string& text, const std::string& text2 = std::string()) override + virtual void onNewImpulseResponse(vector&& data) override { (void)data; } + virtual void onNewNullSymbol(vector&& data) override { (void)data; } + virtual void onConstellationPoints(vector&& data) override { (void)data; } + virtual void onMessage(message_level_t level, const string& text, const string& text2 = string()) override { - std::string fullText; + string fullText; if (text2.empty()) fullText = text; else @@ -288,6 +297,7 @@ struct options_t { int web_port = -1; // positive value means enable list tests; string outputcodec = ""; + string pcm = PCM_DEVICE; RadioReceiverOptions rro; }; @@ -304,7 +314,9 @@ static void usage() endl << "Tuning:" << endl << " -c channel Tune to (eg. 10B, 5A, LD...)." << endl << - " -p programme Play with ALSA (text name of the radio: eg. GRIFF)." << endl << + " -p programme Play with ALSA. The can be either" << endl << + " * a station's label (eg. GRIFF) - or a part of it - or" << endl << + " * a station's Service Id (eg. 0x4f57 or 20311)." << endl << endl << "Dumping:" << endl << " -D Dump FIC and all programmes to files (cannot be used with -C)." << endl << @@ -341,6 +353,7 @@ static void usage() " -A antenna Set input antenna to ANT (for SoapySDR input only)." << endl << " -T Disable TII decoding to reduce CPU usage." << endl << " -O Output Codec for web streaming : mp3 (default), flac (lossless)" << endl << + " -o Specify alsa PCM device by name" << endl << endl << "Other options:" << endl << " -t test_id Run test ." << endl << @@ -409,7 +422,7 @@ options_t parse_cmdline(int argc, char **argv) options.rro.decodeTII = true; int opt; - while ((opt = getopt(argc, argv, "A:c:C:dDf:F:g:hp:O:Ps:Tt:uvw:")) != -1) { + while ((opt = getopt(argc, argv, "A:c:C:dDf:F:g:hp:O:o:Ps:Tt:uvw:")) != -1) { switch (opt) { case 'A': options.antenna = optarg; @@ -418,7 +431,7 @@ options_t parse_cmdline(int argc, char **argv) options.channel = optarg; break; case 'C': - options.num_decoders_in_carousel = std::atoi(optarg); + options.num_decoders_in_carousel = atoi(optarg); break; case 'd': options.dump_programme = true; @@ -433,7 +446,7 @@ options_t parse_cmdline(int argc, char **argv) fe_opt = optarg; break; case 'g': - options.gain = std::atoi(optarg); + options.gain = atoi(optarg); break; case 'p': options.programme = optarg; @@ -441,6 +454,9 @@ options_t parse_cmdline(int argc, char **argv) case 'O': options.outputcodec = optarg; break; + case 'o': + options.pcm = optarg; + break; case 'P': options.carousel_pad = true; break; @@ -451,7 +467,7 @@ options_t parse_cmdline(int argc, char **argv) options.soapySDRDriverArgs = optarg; break; case 't': - options.tests.push_back(std::atoi(optarg)); + options.tests.push_back(atoi(optarg)); break; case 'T': options.rro.decodeTII = false; @@ -462,7 +478,7 @@ options_t parse_cmdline(int argc, char **argv) copyright(); exit(0); case 'w': - options.web_port = std::atoi(optarg); + options.web_port = atoi(optarg); break; case 'u': options.rro.disableCoarseCorrector = true; @@ -490,6 +506,19 @@ options_t parse_cmdline(int argc, char **argv) return options; } +unsigned parse_service_to_tune(const string& name) { + try { + unsigned long id = stoul(name, nullptr, 0); + if (id <= numeric_limits::max()) + return (unsigned)id; + else + return 0; + } + catch (...) { + return 0; + } +}; + int main(int argc, char **argv) { auto options = parse_cmdline(argc, argv); @@ -563,6 +592,7 @@ int main(int argc, char **argv) auto freq = channels.getFrequency(options.channel); in->setFrequency(freq); string service_to_tune = options.programme; + unsigned service_to_tune_idx = parse_service_to_tune(service_to_tune); if (not options.tests.empty()) { Tests tests(in, options.rro); @@ -595,7 +625,7 @@ int main(int argc, char **argv) #ifdef HAVE_FLAC ds.outputCodec = OutputCodec::FLAC; #else - cerr << "Flac support not compiled. Please enable flac support." << std::endl; + cerr << "Flac support not compiled. Please enable flac support." << endl; return 1; #endif } @@ -639,7 +669,7 @@ int main(int argc, char **argv) cerr << "Service list" << endl; for (const auto& s : rx.getServiceList()) { - cerr << " [0x" << std::hex << s.serviceId << std::dec << "] " << + cerr << " [0x" << hex << s.serviceId << dec << "] " << s.serviceLabel.utf8_label() << " "; for (const auto& sc : rx.getComponents(s)) { cerr << " [component " << sc.componentNr << @@ -653,11 +683,11 @@ int main(int argc, char **argv) cerr << endl; string dumpFilePrefix = s.serviceLabel.utf8_label(); - dumpFilePrefix.erase(std::find_if(dumpFilePrefix.rbegin(), dumpFilePrefix.rend(), - [](int ch) { return !std::isspace(ch); }).base(), dumpFilePrefix.end()); + dumpFilePrefix.erase(find_if(dumpFilePrefix.rbegin(), dumpFilePrefix.rend(), + [](int ch) { return !isspace(ch); }).base(), dumpFilePrefix.end()); WavProgrammeHandler ph(s.serviceId, dumpFilePrefix); - phs.emplace(std::make_pair(s.serviceId, move(ph))); + phs.emplace(make_pair(s.serviceId, move(ph))); auto dumpFileName = dumpFilePrefix + ".msc"; @@ -676,11 +706,11 @@ int main(int argc, char **argv) } else { #if defined(HAVE_ALSA) - AlsaProgrammeHandler ph; + AlsaProgrammeHandler ph(options.pcm); while (not service_to_tune.empty()) { cerr << "Service list" << endl; for (const auto& s : rx.getServiceList()) { - cerr << " [0x" << std::hex << s.serviceId << std::dec << "] " << + cerr << " [0x" << hex << s.serviceId << dec << "] " << s.serviceLabel.utf8_label() << " "; for (const auto& sc : rx.getComponents(s)) { cerr << " [component " << sc.componentNr << @@ -696,13 +726,13 @@ int main(int argc, char **argv) bool service_selected = false; for (const auto& s : rx.getServiceList()) { - if (s.serviceLabel.utf8_label().find(service_to_tune) != string::npos) { + if ((service_to_tune_idx && s.serviceId == service_to_tune_idx) || s.serviceLabel.utf8_label().find(service_to_tune) != string::npos) { service_selected = true; string dumpFileName; if (options.dump_programme) { dumpFileName = s.serviceLabel.utf8_label(); - dumpFileName.erase(std::find_if(dumpFileName.rbegin(), dumpFileName.rend(), - [](int ch) { return !std::isspace(ch); }).base(), dumpFileName.end()); + dumpFileName.erase(find_if(dumpFileName.rbegin(), dumpFileName.rend(), + [](int ch) { return !isspace(ch); }).base(), dumpFileName.end()); dumpFileName += ".msc"; } if (rx.playSingleProgramme(ph, dumpFileName, s) == false) { @@ -720,6 +750,7 @@ int main(int argc, char **argv) if (service_to_tune == ".") { break; } + service_to_tune_idx = parse_service_to_tune(service_to_tune); cerr << "**** Trying to tune to " << service_to_tune << endl; } #else diff --git a/tools/android/build-loop.sh b/tools/android/build-loop.sh new file mode 100755 index 00000000..92bdbb2f --- /dev/null +++ b/tools/android/build-loop.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +QT_VER="${QT_VER:-6.5.3}" +QT_ANDROID_PREFIX_ARM64="${QT_ANDROID_PREFIX_ARM64:-$HOME/Qt/$QT_VER/android_arm64_v8a}" +QT_HOST_PATH="${QT_HOST_PATH:-$HOME/Qt/$QT_VER/gcc_64}" +QT_CMAKE_BIN="${QT_CMAKE_BIN:-$QT_HOST_PATH/bin/qt-cmake}" + +MAX_ITERS=6 +ITER=0 + +while true; do + ITER=$((ITER+1)) + echo "== Build attempt $ITER ==" + set +e + QT_ANDROID_PREFIX_ARM64="$QT_ANDROID_PREFIX_ARM64" \ + QT_HOST_PATH="$QT_HOST_PATH" \ + QT_CMAKE_BIN="$QT_CMAKE_BIN" \ + tools/android/build.sh 2>&1 | tee /tmp/welle-android-build.log + RC=${PIPESTATUS[0]} + set -e + if [[ $RC -eq 0 ]]; then + echo "Build succeeded." + exit 0 + fi + + # Detect missing Qt component + MISSING=$(rg -o "Failed to find required Qt component \"([A-Za-z0-9_]+)\"" /tmp/welle-android-build.log | head -n1 | sed -E 's/.*\"(.*)\"/\1/') + if [[ -z "$MISSING" ]]; then + # Alternate form: Could NOT find Qt6Multimedia (missing: Qt6Multimedia_DIR) + MISSING=$(rg -o "Could NOT find (Qt6[A-Za-z0-9_]+)" /tmp/welle-android-build.log | head -n1 | sed -E 's/Could NOT find (Qt6[A-Za-z0-9_]+)/\1/') + fi + + if [[ -z "$MISSING" ]]; then + echo "Build failed but missing Qt module not detected. See /tmp/welle-android-build.log" >&2 + exit 1 + fi + + echo "Missing Qt module detected: $MISSING" + # Strip Qt6 prefix for aqt module name if present + MODULE="$MISSING" + MODULE=${MODULE#Qt6} + + tools/android/install-qt-module.sh "$MODULE" + + if [[ $ITER -ge $MAX_ITERS ]]; then + echo "Max iterations reached" >&2 + exit 1 + fi + +done diff --git a/tools/android/build.sh b/tools/android/build.sh new file mode 100755 index 00000000..df21a06e --- /dev/null +++ b/tools/android/build.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DIST_DIR="$ROOT_DIR/dist" +BUILD_DIR="$ROOT_DIR/build-android-arm64" + +ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}" +if [[ ! -d "$ANDROID_SDK_ROOT" ]]; then + echo "ANDROID_SDK_ROOT not found at $ANDROID_SDK_ROOT" >&2 + exit 1 +fi + +# Pick newest build-tools directory +BUILD_TOOLS_DIR="$(ls -1d "$ANDROID_SDK_ROOT"/build-tools/* 2>/dev/null | sort -V | tail -n1 || true)" +if [[ -z "$BUILD_TOOLS_DIR" ]]; then + echo "No build-tools found under $ANDROID_SDK_ROOT/build-tools" >&2 + exit 1 +fi + +export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$BUILD_TOOLS_DIR:$PATH" + +# Prefer NDK r25b, otherwise newest installed +NDK_DIR="" +if [[ -d "$ANDROID_SDK_ROOT/ndk/25.1.8937393" ]]; then + NDK_DIR="$ANDROID_SDK_ROOT/ndk/25.1.8937393" +else + NDK_DIR="$(ls -1d "$ANDROID_SDK_ROOT"/ndk/* 2>/dev/null | sort -V | tail -n1 || true)" +fi +if [[ -z "$NDK_DIR" ]]; then + echo "No NDK found under $ANDROID_SDK_ROOT/ndk" >&2 + exit 1 +fi + +# Qt Android path must be provided by user or preinstalled +QT_CMAKE_BIN="${QT_CMAKE_BIN:-}" # optional explicit path to qt-cmake +QT_ANDROID_PREFIX_ARM64="${QT_ANDROID_PREFIX_ARM64:-}" # e.g. /opt/Qt/6.5.3/android_arm64_v8a +QT_HOST_PATH="${QT_HOST_PATH:-}" # optional + +if [[ -z "$QT_CMAKE_BIN" ]]; then + if command -v qt-cmake >/dev/null 2>&1; then + QT_CMAKE_BIN="$(command -v qt-cmake)" + elif [[ -x /usr/lib/qt6/bin/qt-cmake ]]; then + QT_CMAKE_BIN="/usr/lib/qt6/bin/qt-cmake" + fi +fi + +if [[ -z "$QT_CMAKE_BIN" || ! -x "$QT_CMAKE_BIN" ]]; then + echo "qt-cmake not found. Set QT_CMAKE_BIN or ensure Qt host tools are installed." >&2 + exit 1 +fi + +if [[ -z "$QT_ANDROID_PREFIX_ARM64" || ! -d "$QT_ANDROID_PREFIX_ARM64" ]]; then + echo "Qt Android arm64 prefix not found. Set QT_ANDROID_PREFIX_ARM64 (e.g. /opt/Qt/6.5.3/android_arm64_v8a)." >&2 + exit 1 +fi + +QT_TOOLCHAIN_FILE="$QT_ANDROID_PREFIX_ARM64/lib/cmake/Qt6/qt.toolchain.cmake" +if [[ ! -f "$QT_TOOLCHAIN_FILE" ]]; then + echo "Qt toolchain file not found at $QT_TOOLCHAIN_FILE" >&2 + exit 1 +fi + +mkdir -p "$BUILD_DIR" "$DIST_DIR" + +ARGS=( + -DCMAKE_TOOLCHAIN_FILE="$QT_TOOLCHAIN_FILE" + -DQT_HOST_PATH="$QT_HOST_PATH" + -DANDROID_SDK_ROOT="$ANDROID_SDK_ROOT" + -DANDROID_NDK_ROOT="$NDK_DIR" + -DANDROID_ABI=arm64-v8a + -DANDROID_PLATFORM=android-34 + -DCMAKE_POLICY_DEFAULT_CMP0057=NEW + -DQT_ENABLE_VERBOSE_DEPLOYMENT=ON + -DCMAKE_VERBOSE_MAKEFILE=ON +) + +"$QT_CMAKE_BIN" "${ARGS[@]}" -S "$ROOT_DIR" -B "$BUILD_DIR" + +# If offline, force Gradle wrapper to use a locally cached distribution. +LOCAL_GRADLE_ZIP="" +LOCAL_GRADLE_ZIP=$(ls -1 "$HOME/.gradle/wrapper/dists/gradle-8.7-bin"/*/gradle-8.7-bin.zip 2>/dev/null | head -n1 || true) +if [[ -z "$LOCAL_GRADLE_ZIP" ]]; then + LOCAL_GRADLE_ZIP=$(ls -1 "$HOME/.gradle/wrapper/dists/gradle-8.13-bin"/*/gradle-8.13-bin.zip 2>/dev/null | head -n1 || true) +fi + +if [[ -n "$LOCAL_GRADLE_ZIP" ]]; then + TEMPLATE_WRAPPER="$QT_ANDROID_PREFIX_ARM64/src/3rdparty/gradle/gradle/wrapper/gradle-wrapper.properties" + if [[ -f "$TEMPLATE_WRAPPER" ]]; then + sed -i "s|^distributionUrl=.*|distributionUrl=file://$LOCAL_GRADLE_ZIP|" "$TEMPLATE_WRAPPER" + fi + WRAPPER_PROPS="$BUILD_DIR/android-build/gradle/wrapper/gradle-wrapper.properties" + if [[ -f "$WRAPPER_PROPS" ]]; then + sed -i "s|^distributionUrl=.*|distributionUrl=file://$LOCAL_GRADLE_ZIP|" "$WRAPPER_PROPS" + fi +fi + +# Work around Gradle networking/wildcard issues in restricted environments. +GRADLE_PROPS_TEMPLATE="$QT_ANDROID_PREFIX_ARM64/src/3rdparty/gradle/gradle.properties" +if [[ -f "$GRADLE_PROPS_TEMPLATE" ]]; then + if ! rg -q "^org.gradle.jvmargs=.*preferIPv4Stack" "$GRADLE_PROPS_TEMPLATE"; then + printf '\norg.gradle.jvmargs=-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false\n' >> "$GRADLE_PROPS_TEMPLATE" + fi + if ! rg -q "^org.gradle.daemon=false" "$GRADLE_PROPS_TEMPLATE"; then + printf '\norg.gradle.daemon=false\n' >> "$GRADLE_PROPS_TEMPLATE" + fi +fi + +# Force Gradle wrapper to run without daemon (avoid socket bind failures). +GRADLEW_TEMPLATE="$QT_ANDROID_PREFIX_ARM64/src/3rdparty/gradle/gradlew" +if [[ -f "$GRADLEW_TEMPLATE" ]]; then + if ! rg -q -- "--no-daemon" "$GRADLEW_TEMPLATE"; then + sed -i 's/"\$@"/"\$@" --no-daemon/' "$GRADLEW_TEMPLATE" + fi +fi +GRADLEW_BUILD="$BUILD_DIR/android-build/gradlew" +if [[ -f "$GRADLEW_BUILD" ]]; then + if ! rg -q -- "--no-daemon" "$GRADLEW_BUILD"; then + sed -i 's/"\$@"/"\$@" --no-daemon/' "$GRADLEW_BUILD" + fi +fi +GRADLE_PROPS_BUILD="$BUILD_DIR/android-build/gradle.properties" +if [[ -f "$GRADLE_PROPS_BUILD" ]]; then + if ! rg -q "^org.gradle.jvmargs=.*preferIPv4Stack" "$GRADLE_PROPS_BUILD"; then + printf '\norg.gradle.jvmargs=-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false\n' >> "$GRADLE_PROPS_BUILD" + fi + if ! rg -q "^org.gradle.daemon=false" "$GRADLE_PROPS_BUILD"; then + printf '\norg.gradle.daemon=false\n' >> "$GRADLE_PROPS_BUILD" + fi +fi + +# Build APK target (Qt Android) +cmake --build "$BUILD_DIR" --parallel --target apk + +APK_PATH="" +APK_PATH="$(find "$BUILD_DIR" -path "*/android-build/build/outputs/apk/*/*.apk" | head -n1 || true)" +if [[ -z "$APK_PATH" ]]; then + echo "APK not found under $BUILD_DIR" >&2 + exit 1 +fi + +UNSIGNED_APK="$DIST_DIR/welle-io-arm64-v8a-unsigned.apk" +SIGNED_APK="$DIST_DIR/welle-io-arm64-v8a.apk" +cp -f "$APK_PATH" "$UNSIGNED_APK" + +# If a debug keystore is available, sign for local install convenience. +if command -v apksigner >/dev/null 2>&1 && [[ -f "$HOME/.android/debug.keystore" ]]; then + apksigner sign \ + --ks "$HOME/.android/debug.keystore" \ + --ks-pass pass:android \ + --key-pass pass:android \ + --ks-key-alias androiddebugkey \ + --out "$SIGNED_APK" \ + "$UNSIGNED_APK" + echo "APK (signed): $SIGNED_APK" +else + cp -f "$UNSIGNED_APK" "$SIGNED_APK" + echo "APK (unsigned): $SIGNED_APK" +fi diff --git a/tools/android/install-qt-module.sh b/tools/android/install-qt-module.sh new file mode 100755 index 00000000..e3251313 --- /dev/null +++ b/tools/android/install-qt-module.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +QT_VER="${QT_VER:-6.5.3}" +MODULE="$1" +if [[ -z "$MODULE" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +# Ensure aqt is available via local venv +VENV="$HOME/.venvs/aqt" +if [[ ! -x "$VENV/bin/python" ]]; then + python3 -m venv "$VENV" +fi + +# Install aqtinstall if missing +if ! "$VENV/bin/python" -c 'import aqt' >/dev/null 2>&1; then + "$VENV/bin/pip" install -U pip aqtinstall +fi + +AQT="$VENV/bin/aqt" + +"$AQT" install-qt linux desktop "$QT_VER" gcc_64 -m "$MODULE" --outputdir "$HOME/Qt" +"$AQT" install-qt linux android "$QT_VER" android_arm64_v8a -m "$MODULE" --outputdir "$HOME/Qt"