diff --git a/.github/workflows/plugins_build.yml b/.github/workflows/plugins_build.yml index afcbb88..2f81107 100644 --- a/.github/workflows/plugins_build.yml +++ b/.github/workflows/plugins_build.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout Source Code - uses: actions/checkout@v1 + uses: actions/checkout@v7.0.1 with: fetch-depth: 1 submodules: true @@ -86,7 +86,7 @@ jobs: run: iscc /o. .\installer\win64\Installer.iss - name: Upload installer - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: Csound_win_x64-${{env.CSOUND_VERSION}}.${{github.run_number}}-plugins-installer path: ./csound6-plugins-win_x86_64-*.exe diff --git a/.github/workflows/plugins_build_7.yml b/.github/workflows/plugins_build_7.yml index 4d2ca45..a0ec586 100644 --- a/.github/workflows/plugins_build_7.yml +++ b/.github/workflows/plugins_build_7.yml @@ -33,7 +33,7 @@ jobs: release: ${{ steps.check_pr.outputs.has_label }} steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 @@ -61,7 +61,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Install Csound build dependencies run: | @@ -76,7 +76,7 @@ jobs: - name: Cache Csound install id: cache-csound - uses: actions/cache@v4 + uses: actions/cache@v6.1.0 with: path: ~/csound-install key: csound-${{ runner.os }}-${{ runner.arch }}-${{ env.CSOUND_VERSION }} @@ -122,7 +122,7 @@ jobs: cp 7.x/src/stk/rawwaves/*.raw package/stk/rawwaves/ - name: Upload plugins - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: csound7-plugins-linux path: package/ @@ -134,7 +134,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Install Csound build dependencies env: @@ -151,7 +151,7 @@ jobs: run: 7.x/vcpkg/bootstrap-vcpkg.sh -disableMetrics - name: Cache vcpkg binary cache - uses: actions/cache@v4 + uses: actions/cache@v6.1.0 with: path: vcpkg-cache key: vcpkg-macos-${{ github.run_id }} @@ -159,7 +159,7 @@ jobs: - name: Cache Csound install id: cache-csound - uses: actions/cache@v4 + uses: actions/cache@v6.1.0 with: path: ~/csound-install key: csound-${{ runner.os }}-${{ runner.arch }}-${{ env.CSOUND_VERSION }} @@ -228,7 +228,7 @@ jobs: cp 7.x/src/stk/rawwaves/*.raw package/stk/rawwaves/ - name: Upload plugins - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: csound7-plugins-macos path: package/ @@ -243,7 +243,7 @@ jobs: shell: pwsh steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Install Csound build dependencies run: choco install -y winflexbison3 @@ -255,7 +255,7 @@ jobs: - name: Cache Csound install id: cache-csound - uses: actions/cache@v4 + uses: actions/cache@v6.1.0 with: path: ${{ runner.temp }}\csound-install key: csound-${{ runner.os }}-${{ runner.arch }}-${{ env.CSOUND_VERSION }} @@ -271,7 +271,7 @@ jobs: run: .\7.x\vcpkg\bootstrap-vcpkg.bat -disableMetrics - name: Cache vcpkg binary cache - uses: actions/cache@v4 + uses: actions/cache@v6.1.0 with: path: ${{ github.workspace }}\vcpkg-cache key: vcpkg-windows-${{ github.run_id }} @@ -330,7 +330,7 @@ jobs: Copy-Item '7.x\src\stk\rawwaves\*.raw' -Destination $rawwaves - name: Upload plugins - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: csound7-plugins-windows path: package/ @@ -344,7 +344,7 @@ jobs: image: nvidia/cuda:12.4.1-devel-ubuntu22.04 steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/checkout@v7.0.1 - name: Install build dependencies run: | @@ -360,7 +360,7 @@ jobs: - name: Cache Csound install id: cache-csound - uses: actions/cache@v4 + uses: actions/cache@v6.1.0 with: path: /csound-install key: csound-cuda-${{ runner.os }}-${{ runner.arch }}-${{ env.CSOUND_VERSION }} @@ -403,7 +403,7 @@ jobs: cp build/src/CUDA/*.so package/ - name: Upload plugins - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7.0.2 with: name: csound7-plugins-linux-cuda path: package/ @@ -418,25 +418,25 @@ jobs: contents: write steps: - name: Download Linux plugins - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: csound7-plugins-linux path: release/linux - name: Download Linux CUDA plugins - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: csound7-plugins-linux-cuda path: release/linux - name: Download macOS plugins - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: csound7-plugins-macos path: release/macos - name: Download Windows plugins - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8.0.2 with: name: csound7-plugins-windows path: release/windows diff --git a/.github/workflows/plugins_wasm.yml b/.github/workflows/plugins_wasm.yml new file mode 100644 index 0000000..c783d6d --- /dev/null +++ b/.github/workflows/plugins_wasm.yml @@ -0,0 +1,45 @@ +name: Csound 7 WASM plugins + +on: + pull_request: + branches: [develop] + push: + branches: [develop] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + package: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@v7.1.0 + with: + node-version: 22 + cache: npm + - uses: cachix/install-nix-action@v31.11.1 + - uses: cachix/cachix-action@v17 + with: + name: csound + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + skipPush: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} + - name: Install JS dependencies + run: npm ci + - name: Build WASM plugins with CMake and Nix + run: npm run build + - name: Test and pack + run: npm pack + - uses: actions/upload-artifact@v7.0.2 + with: + name: csound-plugins-npm + path: csound-plugins-*.tgz + if-no-files-found: error diff --git a/.gitignore b/.gitignore index b80e235..d91c3aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ build dist csound6-plugins-win_x86_64-* +node_modules/ +result* +*.tgz +.csound/ +.wasm-dist.*/ diff --git a/7.x/CMakeLists.txt b/7.x/CMakeLists.txt index ed75642..10e1bd2 100644 --- a/7.x/CMakeLists.txt +++ b/7.x/CMakeLists.txt @@ -34,6 +34,12 @@ endif() project(Csound-plugins) +string(TOUPPER "${CMAKE_SYSTEM_NAME}" CMAKE_SYSTEM_NAME_UPPER) +if(CMAKE_SYSTEM_NAME_UPPER STREQUAL "WASI") + set(WASM ON) + include(cmake/WasmPlugins.cmake) +endif() + if (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") set(CMAKE_COMPILER_IS_CLANG 1) endif() @@ -50,6 +56,9 @@ find_package(CSOUND 7.0) if(NOT CSOUND_FOUND) message(FATAL_ERROR "Csound 7.0 or newer is required") endif() +if(WASM AND NOT CSOUND_VERSION_MAJOR EQUAL 7) + message(FATAL_ERROR "WASM plugins require the Csound 7 SDK") +endif() include(TestBigEndian) include(CheckFunctionExists) @@ -163,7 +172,9 @@ if(LINUX) endif() # plugin install directories (CS_USER_PLUGIN on MacOs/Windows) -if(USE_DOUBLE) +if(WASM) + set(PLUGIN_INSTALL_DIR "lib") +elseif(USE_DOUBLE) message(STATUS "Building with 64-bit floats") if(APPLE) set(PLUGIN_INSTALL_DIR "$ENV{HOME}/Library/csound/${APIVERSION}/plugins64") @@ -232,7 +243,11 @@ endfunction(check_deps) # NB - this was moved here as it needs some VARS defined above # for setting up the framework function(make_plugin libname srcs) - if(APPLE) + if(WASM) + # CMake does not model WASI side modules as shared libraries. + add_executable(${libname} ${srcs}) + configure_wasm_plugin(${libname}) + elseif(APPLE) add_library(${libname} SHARED ${srcs}) else() add_library(${libname} MODULE ${srcs}) @@ -245,6 +260,7 @@ function(make_plugin libname srcs) endwhile() install(TARGETS ${libname} + RUNTIME DESTINATION "${PLUGIN_INSTALL_DIR}" LIBRARY DESTINATION "${PLUGIN_INSTALL_DIR}" ARCHIVE DESTINATION "${PLUGIN_INSTALL_DIR}" ) endfunction(make_plugin) diff --git a/7.x/cmake/Modules/FindCSOUND.cmake b/7.x/cmake/Modules/FindCSOUND.cmake index 66232fe..a0dec67 100644 --- a/7.x/cmake/Modules/FindCSOUND.cmake +++ b/7.x/cmake/Modules/FindCSOUND.cmake @@ -82,8 +82,14 @@ include(FindPackageHandleStandardArgs) # handle the QUIETLY and REQUIRED arguments and set CSOUND_FOUND to TRUE # if all listed variables are TRUE and the version satisfies the request +if(WASM) + # Side modules call the browser engine through its API table. + set(_csound_required_vars CSOUND_INCLUDE_DIR) +else() + set(_csound_required_vars CSOUND_LIBRARY CSOUND_INCLUDE_DIR) +endif() find_package_handle_standard_args(CSOUND - REQUIRED_VARS CSOUND_LIBRARY CSOUND_INCLUDE_DIR + REQUIRED_VARS ${_csound_required_vars} VERSION_VAR CSOUND_VERSION ) diff --git a/7.x/cmake/WasmPlugins.cmake b/7.x/cmake/WasmPlugins.cmake new file mode 100644 index 0000000..280f685 --- /dev/null +++ b/7.x/cmake/WasmPlugins.cmake @@ -0,0 +1,37 @@ +# Match platform/wasm-wasi/src/plugin_example*.nix in csound/csound. +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(USE_COMPILER_OPTIMIZATIONS OFF CACHE BOOL "" FORCE) +set(REQUIRE_PTHREADS OFF CACHE BOOL "" FORCE) +set(USE_DOUBLE ON CACHE BOOL "" FORCE) +execute_process(COMMAND ${CMAKE_C_COMPILER} --print-libgcc-file-name + OUTPUT_VARIABLE WASM_BUILTINS OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + +function(configure_wasm_plugin target) + set_target_properties(${target} PROPERTIES SUFFIX ".wasm") + target_compile_features(${target} PRIVATE c_std_11) + target_compile_definitions(${target} PRIVATE + USE_DOUBLE=1 _WASI_EMULATED_SIGNAL _WASI_EMULATED_MMAN) + target_compile_options(${target} PRIVATE + -fPIC "SHELL:-mllvm -wasm-enable-sjlj" + "$<$:-std=gnu11>" + "$<$:-fno-exceptions;-nostdinc++>" + "$<$:-isystem${WASM_CXX_INCLUDE_DIR}>") + target_link_options(${target} PRIVATE + -shared -nostdlib -nostartfiles + "LINKER:-Bsymbolic" + "LINKER:--experimental-pic" + "LINKER:--no-entry" + "LINKER:--import-table" + "LINKER:--import-memory" + "LINKER:--allow-undefined" + "LINKER:--export=__wasm_call_ctors" + "LINKER:--export-if-defined=__wasm_apply_data_relocs" + "LINKER:--export-if-defined=csound_opcode_init" + "LINKER:--export-if-defined=csoundModuleInfo" + "LINKER:--export-if-defined=csoundModuleInit" + "LINKER:--export-if-defined=csoundModuleCreate" + "LINKER:--export-if-defined=csoundModuleDestroy") + target_link_libraries(${target} PRIVATE + "${WASM_LIBCXX}" "${WASM_LIBC}" "${WASM_SETJMP}" "${WASM_BUILTINS}") +endfunction() diff --git a/README.md b/README.md index 457805f..7be5104 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ plugins. The tree is currently organised by major Csound versions. Full and beta releases are available and build artefacts are also accessible from the actions tab. +The Csound 7 WASM package, `@csound/plugins`, uses Nix and CMake. See the +[build, test, and npm release notes](wasm/README.md). + NB: Csound 6.x is EOL. It is likely that plugins for that version will be removed from the develop and main branch after the first full release. In that case, we will keep the code solely in the csound6 branch. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..9b8d546 --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,20 @@ +# Third-party code in the WASM package + +The plugins use the LGPL-2.1-or-later license in `LICENSE`. Their source and +copyright notices live under [`7.x/src`](https://github.com/csound/plugins/tree/develop/7.x/src). +The package also contains code from these libraries: + +| Library | Used by | License notices | +| --- | --- | --- | +| Eigen | Chua | `dist/licenses/eigen/` | +| GMM++ (GetFEM) | Linear algebra | `dist/licenses/gmm.txt`; LGPL-3.0-or-later with the exception in the headers | +| libpng | Image | `dist/licenses/libpng.txt` | +| zlib | Image | `dist/licenses/zlib.txt` | +| wasi-libc, musl, cloudlibc, musl-fts | C runtime | `dist/licenses/wasi-libc/` | +| LLVM libc++, libc++abi, compiler-rt | C++ runtime and compiler support | `dist/licenses/libcxx.txt`, `libcxxabi.txt`, `compiler-rt.txt` | + +The Nix build copies these notices from the source used to build each library. +[`wasm/nixpkgs.nix`](https://github.com/csound/plugins/blob/develop/wasm/nixpkgs.nix) +pins their source versions. The GMM++ headers live in this repository. The build +uses upstream CMake and make options for static PIC libraries; it adds no source +patches. `wasm/default.nix` records those options. diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..92dc222 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,6 @@ +/** Asset URLs for Csound 7's `withPlugins` option. */ +export declare const plugins: Readonly<{ + chua: string; + image: string; + linearAlgebra: string; +}>; diff --git a/index.js b/index.js new file mode 100644 index 0000000..312756a --- /dev/null +++ b/index.js @@ -0,0 +1,6 @@ +/** Asset URLs for Csound 7's `withPlugins` option. */ +export const plugins = Object.freeze({ + chua: new URL("./dist/chua.wasm", import.meta.url).href, + image: new URL("./dist/image.wasm", import.meta.url).href, + linearAlgebra: new URL("./dist/linear_algebra.wasm", import.meta.url).href, +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1f9e10f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,569 @@ +{ + "name": "@csound/plugins", + "version": "7.0.0-beta.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@csound/plugins", + "version": "7.0.0-beta.0", + "license": "LGPL-2.1-or-later", + "devDependencies": { + "@csound/browser": "7.0.0-beta39", + "@csound/wasm-bin": "7.0.0-beta29" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csound/browser": { + "version": "7.0.0-beta39", + "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta39.tgz", + "integrity": "sha512-/POVGpLfRrQ3kVjCPBp5bTnfYgMUQRCUHtrAilwSjrlRMQbQvQw3qjtcNbwTAxdY6uhppbpy7GbUAU2kl+OHug==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "eventemitter3": "^5.0.4", + "google-closure-compiler": "20260923.0.0", + "google-closure-library": "^20230802.0.0", + "pako": "^3.0.2", + "ramda": "^0.32.0", + "rimraf": "^6.1.3", + "standardized-audio-context": "^25.3.77", + "text-encoding-shim": "^1.0.5", + "unmute-ios-audio": "^3.3.0", + "web-midi-api": "^2.4.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@csound/wasm-bin": { + "version": "7.0.0-beta29", + "resolved": "https://registry.npmjs.org/@csound/wasm-bin/-/wasm-bin-7.0.0-beta29.tgz", + "integrity": "sha512-OfrrLaS+h6Br7S9XaJWK9tBxj1JgPRxEySYx2XyZjgAnt+UIn9Dm8H2qBSJUJgBWIT2KBmFCyw4fcdcwtGGyOg==", + "dev": true, + "license": "LGPL-2.1" + }, + "node_modules/automation-events": { + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-7.1.19.tgz", + "integrity": "sha512-cD+TLhJTI0q4AI3ktd353lrGZiVa9AchowSDzQzzGjSoYe22js4vlS32VUtWuaulghi1Yq0KYNWKk9wWuGymPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-closure-compiler": { + "version": "20260923.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20260923.0.0.tgz", + "integrity": "sha512-45HDxy12r6D6E4nLqB/ae7qzX37x+qHYjIMX7ozYJcAeJoWx8EKxNHTP//1I+Phu62ZPW2CgLe5nq0R6xGHsDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 <5.6.1 || ^5.6.2 >5.6.1", + "google-closure-compiler-java": "^20260923.0.0", + "minimist": "^1.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "bin": { + "google-closure-compiler": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "google-closure-compiler-linux": "^20260923.0.0", + "google-closure-compiler-linux-arm64": "^20260923.0.0", + "google-closure-compiler-macos": "^20260923.0.0", + "google-closure-compiler-windows": "^20260923.0.0" + } + }, + "node_modules/google-closure-compiler-java": { + "version": "20260923.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20260923.0.0.tgz", + "integrity": "sha512-ySzXGXao466rqU1Bpkl/DrE7D8fo/dxkHsN1EnZ5mzr5uEM533FP16+yM6jZfAbjOcWEc48y1LzY2yFhMBsn4w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/google-closure-compiler-linux": { + "version": "20260923.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20260923.0.0.tgz", + "integrity": "sha512-dO0M/kFl1U2/IPphilHmA/tUshrJ9p9TBYbqXbeaFYs0/yo/515f3qoRelGwc/EC5LB9+X1aHwiiwd+9DK1+9g==", + "cpu": [ + "x32", + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/google-closure-compiler-linux-arm64": { + "version": "20260923.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20260923.0.0.tgz", + "integrity": "sha512-BeuzHqu9s408xTkIuYymC+dTSOMiQvmQVGhxXkrM0SAy/aE4/Vjn6M3jRfDu4kNdzoPj3f7lbiegS20pZq/wFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/google-closure-compiler-macos": { + "version": "20260923.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20260923.0.0.tgz", + "integrity": "sha512-MnpLL5puaSU3fJtfe9DytNfPwMXQA73AqsYM0BrVlAfshKaW2I5fb11oizGbKUNcVRW5Pd9x+60rKePdTl57rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/google-closure-compiler-windows": { + "version": "20260923.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20260923.0.0.tgz", + "integrity": "sha512-+yMoYGT0mre78YZe1RoQc4DLj7SSC423quXdslHogKXT6aXjLeP7+yPhpbBPo4ayikrGuNA3WVsoeCa/OjuAXA==", + "cpu": [ + "x32", + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/google-closure-library": { + "version": "20230802.0.0", + "resolved": "https://registry.npmjs.org/google-closure-library/-/google-closure-library-20230802.0.0.tgz", + "integrity": "sha512-h2lBEX2tjWwfR+dhFukGER15vKY+4cA3nRqwKSOsBdyizlOxTAHOWbEYiGnMoom3/SrVeQ5uG0eKsbxQ3wbsxA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/jazz-midi": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/jazz-midi/-/jazz-midi-1.7.9.tgz", + "integrity": "sha512-c8c4BBgwxdsIr1iVm53nadCrtH7BUlnX3V95ciK/gbvXN/ndE5+POskBalXgqlc/r9p2XUbdLTrgrC6fou5p9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/jzz": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/jzz/-/jzz-1.9.6.tgz", + "integrity": "sha512-J7ENLhXwfm2BNDKRUrL8eKtPhUS/CtMBpiafxQHDBcOWSocLhearDKEdh+ylnZFcr5OXWTed0gj6l/txeQA9vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jazz-midi": "^1.7.9" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pako/-/pako-3.0.2.tgz", + "integrity": "sha512-uBv6IT2aT1A78iU6dpNEbf6+CyhlV/6g9JlJs9kpgjFGFhruIICVRysF/W0SLzXg5+hCl+KroH7e4YUyfEmgLg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ramda": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.32.0.tgz", + "integrity": "sha512-GQWAHhxhxWBWA8oIBr1XahFVjQ9Fic6MK9ikijfd4TZHfE2+urfk+irVlR5VOn48uwMgM+loRRBJd6Yjsbc0zQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/standardized-audio-context": { + "version": "25.3.77", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.77.tgz", + "integrity": "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.6", + "automation-events": "^7.0.9", + "tslib": "^2.7.0" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-encoding-shim": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/text-encoding-shim/-/text-encoding-shim-1.0.5.tgz", + "integrity": "sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/unmute-ios-audio": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unmute-ios-audio/-/unmute-ios-audio-3.3.0.tgz", + "integrity": "sha512-MmoCOrsS2gn3wLT2tT+hF56Q4V4kksIKn2LHrwAtX6umzQwQHDWSh1slMzH+0WuxTZ62s3w8/wsfIII1FQ7ACg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "source-map": "^0.5.1" + } + }, + "node_modules/web-midi-api": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/web-midi-api/-/web-midi-api-2.4.0.tgz", + "integrity": "sha512-tTfLdxa5LpOP1NgWByV458iYKgSLhlsIwqCpfbcJuyjProNtuf5UnX97K4JNyuQCqkR+6thQAIsk2BOMSrKaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jzz": "^1.9.5" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5242717 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "@csound/plugins", + "version": "7.0.0-beta.0", + "private": true, + "description": "Csound 7 opcode plugins for WebAssembly", + "type": "module", + "license": "LGPL-2.1-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/csound/plugins.git" + }, + "bugs": { + "url": "https://github.com/csound/plugins/issues" + }, + "homepage": "https://github.com/csound/plugins", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=22.13.0" + }, + "files": [ + "dist", + "index.js", + "index.d.ts", + "LICENSE", + "THIRD_PARTY.md", + "wasm/README.md" + ], + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js" + }, + "./dist/*": "./dist/*", + "./package.json": "./package.json" + }, + "scripts": { + "build": "bash wasm/scripts/build.sh", + "test": "node --test wasm/tests/*.test.js", + "prepack": "npm test" + }, + "devDependencies": { + "@csound/browser": "7.0.0-beta39", + "@csound/wasm-bin": "7.0.0-beta29" + }, + "keywords": [ + "csound", + "audio", + "plugins", + "wasm", + "wasi", + "webassembly" + ] +} diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000..72ce0e8 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,91 @@ +# @csound/plugins + +WASM opcode plugins for the double-precision Csound 7 browser engine. + +This first package includes Chua (`chuap`), image, and linear algebra. This build +disables STK, FluidSynth, HDF5, Python, WebSockets, device, and GPU plugins. +Csound 6 is not supported. + +## Runtime dependency + +Builds use the plugin SDK from `@csound/wasm-bin`, and tests use the published +`@csound/browser` runtime. [package.json](../package.json) and +[package-lock.json](../package-lock.json) record the versions used together. + +## Use + +Use a compatible `@csound/browser` release, such as the version pinned in +[package.json](../package.json): + +```js +import { Csound } from "@csound/browser"; +import { plugins } from "@csound/plugins"; + +const csound = await Csound({ withPlugins: [plugins.chua, plugins.image] }); +``` + +The exports are asset URL strings. Bundlers must copy the WASM files referenced +by `new URL(..., import.meta.url)`. You can also serve files from +`@csound/plugins/dist/` yourself and pass their URLs to `withPlugins`. + +To use `--opcode-lib`, write the plugin to Csound's virtual filesystem before +compiling your CSD. This does not need `withPlugins`: + +```js +const csound = await Csound(); +const response = await fetch(plugins.image); +if (!response.ok) throw new Error(`Plugin download failed: ${response.status}`); +await csound.fs.writeFile("/image.wasm", new Uint8Array(await response.arrayBuffer())); +await csound.setOption("--opcode-lib=/image.wasm"); +await csound.compileCSD(csd); +``` + +You can also put the option in ``. Its path refers to the virtual +filesystem, not a URL. + +## Build and test + +Install Nix and a Node.js version that meets [package.json](../package.json)'s +`engines` requirement, then run: + +```sh +npm ci +npm run build +npm test +npm pack +``` + +Nix pins the toolchain and builds the existing `7.x` CMake targets with Ninja. +Dependencies use static PIC builds. The C runtime imports the engine's allocator; +it does not link its own heap. C++ plugins use libc++ without C++ exceptions. +To use another Csound 7 SDK, set `CSOUND_PLUGIN_SDK` to an absolute archive path. +Set `NIX_SYSTEM=x86_64-linux` when using a Linux remote builder from macOS. + +Tests load all plugins together, render Chua, compute a vector norm, and save and +reload a PNG through the WASI filesystem. Packing runs the tests and includes +the WASM files and their license notices. For work on the browser loader, +`CSOUND_BROWSER_URL` can point to an absolute file URL of a local browser bundle; +leave it unset to test the published package as CI does. + +## Updating dependencies + +Keep version pins in these files, rather than copying them into commands or docs: + +- [package.json](../package.json) and [package-lock.json](../package-lock.json): + update npm dependencies with `npm install --save-dev --save-exact @`. + The Nix build reads the SDK version, URL, and hash from the lockfile. +- [nixpkgs.nix](nixpkgs.nix): the Nix toolchain revision and hash, shared by the + plugin build and its static dependencies. + +After an update, run the build, test, and pack commands above. The WASM workflow +uses the `csound` Cachix cache and uploads builds when `CACHIX_AUTH_TOKEN` is +available. Fork PRs read from the cache without the secret. Native builds use +GitHub Actions caches. + +## First npm release + +Run the build, test, and pack commands against the published browser package +with `CSOUND_BROWSER_URL` unset. When ready to publish, remove `private` from +`package.json`, set the package version with `npm version`, and run +`npm publish --tag beta --access public` from a clean build. No workflow publishes +this package yet. diff --git a/wasm/default.nix b/wasm/default.nix new file mode 100644 index 0000000..229265c --- /dev/null +++ b/wasm/default.nix @@ -0,0 +1,107 @@ +{ system ? builtins.currentSystem +, pkgs ? import (import ./nixpkgs.nix) { inherit system; } +, csoundSdkArchive ? null +}: +let + wasi = pkgs.pkgsCross.wasi32.clangStdenv; + libc = pkgs.pkgsCross.wasi32.wasilibc.overrideAttrs (old: { + NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or []) ++ [ "-fPIC" ]; + # Use the engine's allocator. wasi-libc also skips its fixed symbol list + # for this mode; PIC adds imports absent from that non-PIC list. + makeFlags = (old.makeFlags or []) ++ [ "MALLOC_IMPL=none" ]; + preFixup = ""; + dontStrip = true; + }); + libcxx = pkgs.pkgsCross.wasi32.llvmPackages.libcxx.overrideAttrs (old: { + cmakeFlags = old.cmakeFlags ++ [ + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" + "-DLIBCXX_ENABLE_FILESYSTEM=ON" + ]; + dontStrip = true; + }); + lock = builtins.fromJSON (builtins.readFile ../package-lock.json); + wasmBin = lock.packages."node_modules/@csound/wasm-bin"; + sdkArchive = if csoundSdkArchive == null then null else builtins.path { + path = csoundSdkArchive; + name = "csound-plugin-sdk.tar.gz"; + }; + sdk = pkgs.runCommand "csound-wasm-plugin-sdk-${wasmBin.version}" {} '' + mkdir -p $out + ${if csoundSdkArchive == null then '' + tar -xzf ${pkgs.fetchurl { url = wasmBin.resolved; hash = wasmBin.integrity; }} + tar -xzf package/lib/csound-plugin-sdk.tar.gz --strip-components=1 -C $out + '' else '' + tar -xzf ${sdkArchive} --strip-components=1 -C $out + ''} + ''; + zlib = wasi.mkDerivation { + pname = "zlib-wasm-pic"; + inherit (pkgs.zlib) version src; + nativeBuildInputs = [ pkgs.cmake pkgs.ninja ]; + cmakeFlags = [ "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" "-DCMAKE_INSTALL_LIBDIR=lib" "-DZLIB_BUILD_EXAMPLES=OFF" ]; + dontStrip = true; + }; + libpng = wasi.mkDerivation { + pname = "libpng-wasm-pic"; + inherit (pkgs.libpng) version src; + nativeBuildInputs = [ pkgs.cmake pkgs.ninja ]; + buildInputs = [ zlib ]; + cmakeFlags = [ + "-DUNIX=ON" + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" + "-DCMAKE_INSTALL_LIBDIR=lib" + "-DPNG_SHARED=OFF" "-DPNG_STATIC=ON" "-DPNG_TESTS=OFF" "-DPNG_TOOLS=OFF" + "-DZLIB_LIBRARY=${zlib}/lib/libzlibstatic.a" + ]; + NIX_CFLAGS_COMPILE = [ "-mllvm" "-wasm-enable-sjlj" ]; + dontStrip = true; + }; +in wasi.mkDerivation { + pname = "csound-plugins-wasm"; + version = (builtins.fromJSON (builtins.readFile ../package.json)).version; + src = pkgs.lib.cleanSource ../7.x; + nativeBuildInputs = [ pkgs.cmake pkgs.ninja ]; + buildInputs = [ libpng zlib ]; + dontStrip = true; + cmakeFlags = [ + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + "-DCMAKE_BUILD_TYPE=Release" + "-DCSOUND_INCLUDE_DIR=${sdk}/include/csound" + "-DWASM_LIBC=${libc}/lib/libc.a" + "-DWASM_SETJMP=${libc}/lib/libsetjmp.a" + "-DWASM_LIBCXX=${libcxx}/lib/libc++.a" + "-DWASM_CXX_INCLUDE_DIR=${libcxx.dev}/include/c++/v1" + "-DEIGEN3_INCLUDE_DIR=${pkgs.eigen}/include/eigen3" + "-DZLIB_LIBRARY=${zlib}/lib/libzlibstatic.a" + "-DPNG_LIBRARY=${libpng}/lib/libpng.a" + "-DBUILD_CHUA_OPCODES=ON" + "-DBUILD_IMAGE_OPCODES=ON" + "-DBUILD_LINEAR_ALGEBRA_OPCODES=ON" + "-DBUILD_STK_OPCODES=OFF" + "-DBUILD_FLUID_OPCODES=OFF" + "-DBUILD_HDF5_OPCODES=OFF" + "-DBUILD_WEBSOCKET_OPCODE=OFF" + "-DBUILD_PYTHON_OPCODES=OFF" + "-DBUILD_WIIMOTE_OPCODES=OFF" + "-DBUILD_P5GLOVE_OPCODES=OFF" + "-DBUILD_CUDA_OPCODES=OFF" + "-DBUILD_OPENCL_OPCODES=OFF" + ]; + postInstall = '' + mkdir -p $out/share/licenses/wasi-libc $out/share/licenses/eigen + cp ${libc.src}/LICENSE* $out/share/licenses/wasi-libc/ + cp ${libc.src}/libc-top-half/musl/COPYRIGHT $out/share/licenses/wasi-libc/musl.txt + cp ${libc.src}/libc-bottom-half/cloudlibc/LICENSE $out/share/licenses/wasi-libc/cloudlibc.txt + cp ${libc.src}/fts/musl-fts/COPYING $out/share/licenses/wasi-libc/musl-fts.txt + cp ${libcxx.src}/libcxx/LICENSE.TXT $out/share/licenses/libcxx.txt + cp ${libcxx.src}/libcxxabi/LICENSE.TXT $out/share/licenses/libcxxabi.txt + cp ${pkgs.pkgsCross.wasi32.llvmPackages.compiler-rt.src}/compiler-rt/LICENSE.TXT $out/share/licenses/compiler-rt.txt + cp ${pkgs.eigen.src}/COPYING* $out/share/licenses/eigen/ + sed -n '1,/^=*[*][/]$/p' ${../7.x/src/linear_algebra/gmm/gmm.h} > $out/share/licenses/gmm.txt + tar -xOf ${pkgs.libpng.src} libpng-${pkgs.libpng.version}/LICENSE > $out/share/licenses/libpng.txt + tar -xOf ${pkgs.zlib.src} zlib-${pkgs.zlib.version}/LICENSE > $out/share/licenses/zlib.txt + for plugin in chua image linear_algebra; do + test -s "$out/lib/$plugin.wasm" + done + ''; +} diff --git a/wasm/nixpkgs.nix b/wasm/nixpkgs.nix new file mode 100644 index 0000000..89b6f90 --- /dev/null +++ b/wasm/nixpkgs.nix @@ -0,0 +1,5 @@ +# Keep in sync with csound/csound's platform/wasm-wasi/scripts/nixpkgs-pin.sh. +builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/4db2c220f32fd162658ed1b7bb2f46a82996ddbe.tar.gz"; + sha256 = "08b1k0zrbal2if94bph2ap9f73zlplsh05a65jbdkp5ipycmg92s"; +} diff --git a/wasm/scripts/build.sh b/wasm/scripts/build.sh new file mode 100644 index 0000000..22c5070 --- /dev/null +++ b/wasm/scripts/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/../.." + +args=(--show-trace -o result-wasm) +if [[ -n "${NIX_SYSTEM:-}" ]]; then + args+=(--argstr system "$NIX_SYSTEM") +fi +if [[ -n "${CSOUND_PLUGIN_SDK:-}" ]]; then + args+=(--argstr csoundSdkArchive "$CSOUND_PLUGIN_SDK") +fi +nix-build wasm/default.nix "${args[@]}" +staging=$(mktemp -d .wasm-dist.XXXXXX) +trap 'rm -rf "$staging"' EXIT +for plugin in chua image linear_algebra; do + install -m 644 "result-wasm/lib/$plugin.wasm" "$staging/$plugin.wasm" +done +cp -R result-wasm/share/licenses "$staging/licenses" +chmod -R u+w "$staging/licenses" +if [[ -d dist ]]; then + chmod -R u+w dist +fi +rm -rf dist +mv "$staging" dist diff --git a/wasm/tests/runtime.test.js b/wasm/tests/runtime.test.js new file mode 100644 index 0000000..783c3c9 --- /dev/null +++ b/wasm/tests/runtime.test.js @@ -0,0 +1,122 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import { plugins } from "../../index.js"; + +// The in-process browser API needs these names, but no audio device or DOM. +globalThis.self = globalThis; +globalThis.window = globalThis; +const { libcsound } = await import(process.env.CSOUND_BROWSER_URL || "@csound/browser/dist/csound.js"); + +const binaries = await Promise.all(Object.values(plugins).map((url) => readFile(new URL(url)))); + +const document = (body) => ` + +-n -d -m0 + + +sr = 48000 +ksmps = 32 +nchnls = 1 +0dbfs = 1 +instr 1 +${body} +endin + + +i 1 0 0.02 +e + +`; + +/** + * Load plugin bytes into Csound 7 and render the given instrument body. + * Call check with the API, Csound instance, and peak sample magnitude before + * destroying the instance. + */ +async function run(body, check, withPlugins = binaries) { + const errors = []; + const originalError = console.error; + console.error = (...args) => errors.push(args.join(" ")); + let api; + try { + api = await libcsound({ withPlugins }); + } finally { + console.error = originalError; + } + assert.deepEqual(errors, [], "every plugin must load without missing imports"); + assert.ok(api.csoundGetVersion() >= 7000 && api.csoundGetVersion() < 8000); + assert.equal(api.csoundGetSizeOfMYFLT(), 8); + const csound = api.csoundCreate(); + try { + assert.equal(api.csoundCompileCSD(csound, document(body)), 0); + assert.equal(api.csoundStart(csound), 0); + let peak = 0; + let blocks = 0; + while (api.csoundPerformKsmps(csound) === 0) { + const samples = new Float64Array(api.getMemory().buffer, api.csoundGetSpout(csound), 32); + for (const sample of samples) { + assert.ok(Number.isFinite(sample), "audio samples must be finite"); + peak = Math.max(peak, Math.abs(sample)); + } + assert.ok(++blocks < 100, "score must finish"); + } + assert.ok(blocks > 0); + await check(api, csound, peak); + } finally { + api.csoundDestroy(csound); + } +} + +test("all plugins are WASI side modules", async () => { + for (const bytes of binaries) { + const module = await WebAssembly.compile(bytes); + const imports = WebAssembly.Module.imports(module); + const exports = WebAssembly.Module.exports(module); + assert.ok(WebAssembly.Module.customSections(module, "dylink.0").length > 0); + assert.ok(imports.some(({ kind, name }) => kind === "memory" && name === "memory")); + assert.ok(imports.some(({ kind }) => kind === "table")); + assert.ok(exports.some(({ name }) => name === "__wasm_call_ctors")); + assert.ok(exports.some(({ name }) => ["csoundModuleInit", "csound_opcode_init"].includes(name))); + assert.ok(!exports.some(({ name }) => name === "_start")); + } +}); + +test("chua produces finite, nonzero audio alongside the other plugins", async () => { + await run(` + a1, a2, a3 chuap 0.018, 0, 1e-7, 1/1500, -0.0008, -0.0005, 1, 1e-8, 0, 0, 0.1, 1e-5 + out a3 * 0.1 + `, (_, __, peak) => assert.ok(peak > 0)); +}); + +test("linear algebra computes a vector norm", async () => { + await run(` + iv la_i_vr_create 2 + iv la_i_vr_set 0, 3 + iv la_i_vr_set 1, 4 + inorm la_i_norm_euclid_vr iv + chnset inorm, "norm" + `, (api, csound) => assert.equal(api.csoundGetControlChannel(csound, "norm"), 5)); +}); + +test("image writes and reads a PNG through the WASI filesystem", async () => { + await run(` + ih imagecreate 2, 3 + imagesetpixel ih, 0, 0, 1, 0, 0 + imagesave ih, "roundtrip.png" + iload imageload "roundtrip.png" + iw, ihgt imagesize iload + kred, kgreen, kblue imagegetpixel iload, 0, 0 + chnset iw, "width" + chnset ihgt, "height" + chnset kred, "red" + chnset kgreen, "green" + `, async (api, csound) => { + assert.equal(api.csoundGetControlChannel(csound, "width"), 2); + assert.equal(api.csoundGetControlChannel(csound, "height"), 3); + assert.equal(api.csoundGetControlChannel(csound, "red"), 1); + assert.equal(api.csoundGetControlChannel(csound, "green"), 0); + const bytes = await api.readFile(csound, "roundtrip.png"); + assert.deepEqual([...bytes.slice(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); + }); +});