diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f950ca36..fa9ca202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,3 +56,73 @@ jobs: pushd build cmake --build . --target install -- VERBOSE=1 popd + + windows-arm64: + name: Windows ARM64 build and tests + runs-on: windows-11-arm + steps: + - name: Preserve repository line endings + run: git config --global core.autocrlf input + + - uses: actions/checkout@v4 + + - uses: msys2/setup-msys2@v2 + with: + msystem: CLANGARM64 + # Use available rolling packages; record versions against the baseline. + # An archived package snapshot is required for reproducible builds. + update: true + + - name: Install CLANGARM64 packages + shell: msys2 {0} + run: | + mapfile -t packages < <(sed -n '/^[^#[:space:]][^=]*=/s/=.*//p' ci/windows-arm64-packages.lock) + pacman --noconfirm -S --needed -- "${packages[@]}" + + - name: Build libopenshot-audio (CLANGARM64, ARM64) + shell: msys2 {0} + run: | + cmake -B build -S . -G Ninja \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_INSTALL_PREFIX="$PWD/dist-arm64" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DENABLE_AUDIO_DOCS=OFF + cmake --build build + cmake --install build + cmake -S ci/audio-smoke -B smoke-build -G Ninja \ + -DCMAKE_PREFIX_PATH="$PWD/dist-arm64" -DREQUIRE_ARM64=ON + cmake --build smoke-build + PATH="$PWD/dist-arm64/bin:$PATH" ctest --test-dir smoke-build --output-on-failure --no-tests=error + python -m unittest discover -s ci -p "test_*.py" -v + python ci/validate_arm64_architecture.py \ + --require-native-arm64 \ + --package-baseline ci/windows-arm64-packages.lock \ + --payload-root dist-arm64 \ + --require-payload \ + --json-report build/arm64-report.json + + linux-arm64: + name: Linux ARM64 build and tests + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build libasound2-dev python3 + - name: Build and install audio library + run: | + test "$(uname -m)" = aarch64 + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$PWD/install-arm64" -DENABLE_AUDIO_DOCS=OFF + cmake --build build --parallel 2 + cmake --install build + readelf -h install-arm64/lib/libopenshot-audio.so | grep -Eq 'Machine:.*AArch64' + - name: Test installed audio library and validator + run: | + cmake -S ci/audio-smoke -B smoke-build -G Ninja \ + -DCMAKE_PREFIX_PATH="$PWD/install-arm64" -DREQUIRE_ARM64=ON + cmake --build smoke-build + ctest --test-dir smoke-build --output-on-failure --no-tests=error + python3 -m unittest discover -s ci -p 'test_*.py' -v diff --git a/.gitignore b/.gitignore index 927d8c34..3481775a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ .settings .idea/* cmake-build-debug/* + +# Python validator bytecode +__pycache__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2fac254e..2184d5aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -79,6 +79,48 @@ windows-builder-x64: - tags tags: [windows] +# WINDOWS ARM64 (native, additive lane; existing x64/x86 jobs are unchanged). +# Contract identity: windows-arm64-clangarm64-v1 (design-spec.md). +# Verifies the shared ci/windows-arm64-packages.lock and validates every +# produced .exe/.dll/.pyd, plus the native-process oracle of the build host, +# using ci/validate_arm64_architecture.py (design-amendment-A1 semantics: +# native execution requires pProcessMachine == IMAGE_FILE_MACHINE_UNKNOWN +# and pNativeMachine == IMAGE_FILE_MACHINE_ARM64; every payload PE must be +# machine 0xAA64 independently). +windows-builder-arm64: + stage: build-libopenshot-audio + artifacts: + expire_in: 6 months + paths: + - build/install-arm64/ + - build\arm64-architecture-report.json + script: + - $env:Path = "C:\msys64\clangarm64\bin;C:\msys64\usr\bin;" + $env:Path; + - $env:MSYSTEM = "CLANGARM64" + - New-Item -ItemType Directory -Force -Path build + - cmake -B build -S . -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -D"CMAKE_INSTALL_PREFIX:PATH=build/install-arm64" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -G Ninja -D"CMAKE_BUILD_TYPE:STRING=Release" + - cmake --build build + - ctest --test-dir build --output-on-failure -VV + - cmake --install build + - python -m unittest discover -s ci -p "test_*.py" -v + - python ci\validate_arm64_architecture.py --require-native-arm64 --package-baseline ci\windows-arm64-packages.lock --payload-root build\install-arm64 --require-payload --json-report build\arm64-architecture-report.json + - $PROJECT_VERSION = (Select-String -Path "CMakeLists.txt" -Pattern '^set\(PROJECT_VERSION_FULL "(.*)\"' | %{$_.Matches.Groups[1].value}) + - $PROJECT_SO = (Select-String -Path "CMakeLists.txt" -Pattern '^set\(PROJECT_SO_VERSION (.*)\)' | %{$_.Matches.Groups[1].value}) + - New-Item -path "build/install-arm64/share/" -Name "$CI_PROJECT_NAME.env" -Value "CI_PROJECT_NAME:$CI_PROJECT_NAME`nCI_COMMIT_REF_NAME:$CI_COMMIT_REF_NAME`nCI_COMMIT_SHA:$CI_COMMIT_SHA`nCI_JOB_ID:$CI_JOB_ID`nCI_PIPELINE_ID:$CI_PIPELINE_ID`nVERSION:$PROJECT_VERSION`nSO:$PROJECT_SO`nTARGET_TRIPLET:aarch64-w64-mingw32`nPE_MACHINE:0xAA64" -ItemType file -force + - $PREV_GIT_LABEL=(git describe --tags --abbrev=0 '@^') + - git log "$PREV_GIT_LABEL..@" --oneline --pretty=format:"- %C(auto,yellow)%h%C(auto,magenta)% %C(auto,blue)%>(12,trunc)%ad %C(auto,green)%<(25,trunc)%aN%C(auto,reset)%s%C(auto,red)% gD% D" --date=short > "build/install-arm64/share/$CI_PROJECT_NAME.log" + only: + variables: + - $ENABLE_WINDOWS_ARM64 == "1" + except: + - tags + tags: [windows-arm64] + # Requires release infrastructure's native/virtual Windows Arm64 GitLab + # runner tagged windows-arm64 (design-spec.md "Release-infrastructure + # surface"). Until that runner exists, this job cannot execute; it does + # not weaken or replace the existing windows-builder-x64/x86 jobs. + allow_failure: true + windows-builder-x86: stage: build-libopenshot-audio artifacts: diff --git a/CMakeLists.txt b/CMakeLists.txt index c5fd69e6..34ee9446 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,6 +215,8 @@ if(WIN32) if(TARGET ASIO::SDK) target_link_libraries(openshot-audio PRIVATE ASIO::SDK) set(NEED_ASIO TRUE) + else() + target_compile_definitions(openshot-audio PUBLIC JUCE_ASIO=0) endif() # Order here can be important! # For example, winmm.lib must come before kernel32.lib (if linked) diff --git a/JuceLibraryCode/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h b/JuceLibraryCode/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h index ddff0df8..586416a0 100644 --- a/JuceLibraryCode/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h +++ b/JuceLibraryCode/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h @@ -1193,7 +1193,7 @@ class AudioBuffer jassert (size >= 0); auto channelListSize = (size_t) (numChannels + 1) * sizeof (Type*); - auto requiredSampleAlignment = std::alignment_of_v; + auto requiredSampleAlignment = alignof (Type); size_t alignmentOverflow = channelListSize % requiredSampleAlignment; if (alignmentOverflow != 0) diff --git a/ci/README-arm64.md b/ci/README-arm64.md new file mode 100644 index 00000000..5a8bee17 --- /dev/null +++ b/ci/README-arm64.md @@ -0,0 +1,34 @@ + + +# ARM64 build checks + +The `arm64-support` branch collects build support before integration into +`develop`. Existing Linux, Intel Mac, and Windows x86/x64 jobs remain enabled. + +GitHub automatically runs **Linux ARM64 build and tests** on +`ubuntu-24.04-arm` and **Windows ARM64 build and tests** on `windows-11-arm` +for pushes and pull requests. No repository enable variables are needed, and +failures are reported normally. These checks do not publish releases. + +The production GitLab configuration is unchanged: its optional Windows ARM64 +job still requires `ENABLE_WINDOWS_ARM64=1`, a runner tagged `windows-arm64`, +and MSYS2 CLANGARM64 installed at `C:\msys64`. + +`windows-arm64-packages.lock` is a reference package list. GitHub installs +currently available packages by name, and the validator records actual +versions and reports deviations using `--package-baseline`. GitLab expects +the packages to be provisioned on the runner. Reproducible builds require a +separately archived package snapshot; `--package-lock` retains strict version +checking for such environments. Archive checksums are not verified. + +Native validation requires both the Windows host API result and an ARM64 PE +header on the running Python interpreter. Every produced executable, DLL, +and Python extension is also checked independently. A Linux validator test +pass is not evidence of native Windows execution. + +The audio checks build a consumer of the installed library, apply gain to +stereo samples, and verify a WAV encode/decode round trip without requiring +an audio device. They also run the architecture-validator unit tests. diff --git a/ci/audio-smoke/CMakeLists.txt b/ci/audio-smoke/CMakeLists.txt new file mode 100644 index 00000000..734e4a72 --- /dev/null +++ b/ci/audio-smoke/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 OpenShot Studios, LLC +# SPDX-License-Identifier: LGPL-3.0-or-later + +cmake_minimum_required(VERSION 3.16) +project(AudioSmokeTest LANGUAGES CXX) +find_package(OpenShotAudio REQUIRED) +option(REQUIRE_ARM64 "Require a native ARM64 test executable" OFF) +add_executable(audio-smoke main.cpp) +target_compile_features(audio-smoke PRIVATE cxx_std_17) +target_link_libraries(audio-smoke PRIVATE OpenShot::Audio) +if(REQUIRE_ARM64) + target_compile_definitions(audio-smoke PRIVATE REQUIRE_ARM64=1) +endif() +enable_testing() +add_test(NAME audio-buffer-roundtrip COMMAND audio-smoke) diff --git a/ci/audio-smoke/main.cpp b/ci/audio-smoke/main.cpp new file mode 100644 index 00000000..f28cee68 --- /dev/null +++ b/ci/audio-smoke/main.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 OpenShot Studios, LLC +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include +#include "OpenShotAudio.h" + +#if defined(REQUIRE_ARM64) && !defined(__aarch64__) && !defined(_M_ARM64) +#error The ARM64 smoke test must be compiled for ARM64 +#endif + +int main() +{ + // Exercise the installed library without requiring an audio device. + juce::AudioBuffer samples(2, 128); + for (int i = 0; i < samples.getNumSamples(); ++i) { + samples.setSample(0, i, 0.5f); + samples.setSample(1, i, -0.25f); + } + samples.applyGain(0.5f); + juce::MemoryBlock encoded; + juce::WavAudioFormat format; + auto output = std::make_unique(encoded, false); + std::unique_ptr writer( + format.createWriterFor(output.get(), 48000, 2, 16, {}, 0)); + if (!writer) return 1; + output.release(); // The writer owns the stream after successful creation. + if (!writer->writeFromAudioSampleBuffer(samples, 0, samples.getNumSamples())) return 1; + writer.reset(); // Finalize the WAV header before reading it. + std::unique_ptr reader(format.createReaderFor( + new juce::MemoryInputStream(encoded, false), true)); + if (!reader || reader->numChannels != 2 || reader->sampleRate != 48000 + || reader->lengthInSamples != 128) return 1; + juce::AudioBuffer decoded(2, 128); + if (!reader->read(&decoded, 0, 128, 0, true, true)) return 1; + for (int i = 0; i < decoded.getNumSamples(); ++i) { + if (std::abs(decoded.getSample(0, i) - 0.25f) > 0.0001f + || std::abs(decoded.getSample(1, i) + 0.125f) > 0.0001f) return 1; + } + std::cout << "Installed audio library: buffer gain and WAV round trip passed\n"; + return 0; +} diff --git a/ci/test_validate_arm64_architecture.py b/ci/test_validate_arm64_architecture.py new file mode 100644 index 00000000..9f66ed3b --- /dev/null +++ b/ci/test_validate_arm64_architecture.py @@ -0,0 +1,197 @@ +# SPDX-FileCopyrightText: 2026 OpenShot Studios, LLC +# SPDX-License-Identifier: LGPL-3.0-or-later + +import contextlib +import importlib.util +import io +import os +import struct +import sys +import tempfile +import unittest +from unittest import mock + +VALIDATOR_PATH = os.path.join(os.path.dirname(__file__), "validate_arm64_architecture.py") +SPEC = importlib.util.spec_from_file_location("validate_arm64_architecture", VALIDATOR_PATH) +if SPEC is None or SPEC.loader is None: + raise RuntimeError("Unable to load validator from %s" % VALIDATOR_PATH) +validator = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(validator) + + +def write_pe(path, machine): + data = bytearray(0x80) + data[:2] = b"MZ" + struct.pack_into("