From cdc51c0842aa17c78a284b64d059cfb4e6950730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Tue, 13 Oct 2026 20:27:30 +0300 Subject: [PATCH 1/5] feat(wasm): scaffold @csound/plugins npm package --- .github/workflows/plugins_wasm.yml | 51 +++ .gitignore | 5 + 7.x/CMakeLists.txt | 20 +- 7.x/cmake/Modules/FindCSOUND.cmake | 8 +- 7.x/cmake/WasmPlugins.cmake | 37 ++ README.md | 3 + THIRD_PARTY.md | 20 + index.d.ts | 6 + index.js | 6 + package-lock.json | 569 +++++++++++++++++++++++++++++ package.json | 39 ++ wasm/README.md | 72 ++++ wasm/default.nix | 107 ++++++ wasm/nixpkgs.nix | 5 + wasm/scripts/build-runtime.sh | 24 ++ wasm/scripts/build.sh | 24 ++ wasm/tests/runtime.test.js | 117 ++++++ 17 files changed, 1110 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/plugins_wasm.yml create mode 100644 7.x/cmake/WasmPlugins.cmake create mode 100644 THIRD_PARTY.md create mode 100644 index.d.ts create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 wasm/README.md create mode 100644 wasm/default.nix create mode 100644 wasm/nixpkgs.nix create mode 100644 wasm/scripts/build-runtime.sh create mode 100644 wasm/scripts/build.sh create mode 100644 wasm/tests/runtime.test.js diff --git a/.github/workflows/plugins_wasm.yml b/.github/workflows/plugins_wasm.yml new file mode 100644 index 00000000..c9b7f3fa --- /dev/null +++ b/.github/workflows/plugins_wasm.yml @@ -0,0 +1,51 @@ +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 + # csound/csound#3416 must land and ship before this package can publish. + - uses: actions/checkout@v7.0.1 + with: + repository: csound/csound + ref: dfe33946847fd2fe5fe04948d9ae44ba320c5390 + path: .csound + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + - uses: cachix/install-nix-action@v31.11.1 + - uses: cachix/cachix-action@v17 + with: + name: csound + - name: Install JS dependencies + run: npm ci + - name: Build WASM plugins with CMake and Nix + run: npm run build + - name: Build the required Csound runtime + run: | + npm run build:runtime + echo "CSOUND_BROWSER_URL=file://$GITHUB_WORKSPACE/.csound/platform/wasm-wasi/browser/dist/csound.js" >> "$GITHUB_ENV" + - name: Test and pack + run: npm pack + - uses: actions/upload-artifact@v7.0.1 + with: + name: csound-plugins-npm + path: csound-plugins-*.tgz + if-no-files-found: error diff --git a/.gitignore b/.gitignore index b80e2356..d91c3aa4 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 ed75642c..10e1bd28 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 66232fee..a0dec67e 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 00000000..280f6857 --- /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 457805fa..7be51043 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 00000000..9b8d5464 --- /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 00000000..92dc222a --- /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 00000000..312756a9 --- /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 00000000..168efd8f --- /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-beta38", + "@csound/wasm-bin": "7.0.0-beta28" + }, + "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-beta38", + "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta38.tgz", + "integrity": "sha512-Q64BnO6VMjpPNJgP9VZzfTVKB3VAuN/zfIfqyUSFCjsY2SPw3KUqfpU+e1NxWt8VZEkNvSF23OjqCzR40kqgUg==", + "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-beta28", + "resolved": "https://registry.npmjs.org/@csound/wasm-bin/-/wasm-bin-7.0.0-beta28.tgz", + "integrity": "sha512-HjXP+1XbjiYYRRZGc3EnuBSjP6zFjhFqnR0rk8XK9XOyI/rk+35eqPk49r3PwRgyISlB+pnmHiRKMsP8In4K8w==", + "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 00000000..47170238 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "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", + "build:runtime": "bash wasm/scripts/build-runtime.sh", + "test": "node --test wasm/tests/*.test.js", + "prepack": "npm test" + }, + "devDependencies": { + "@csound/browser": "7.0.0-beta38", + "@csound/wasm-bin": "7.0.0-beta28" + }, + "keywords": ["csound", "audio", "plugins", "wasm", "wasi", "webassembly"] +} diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 00000000..af4a19df --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,72 @@ +# @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 + +This draft depends on [csound/csound#3416](https://github.com/csound/csound/pull/3416). +`@csound/browser@7.0.0-beta38` cannot load these plugins. CI builds the upstream +commit in that PR and tests against it. The package stays private until a browser +release includes those changes. + +## Use + +With a compatible browser build: + +```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`. + +## Build and test + +Install Nix and Node.js 22.13 or newer, then run: + +```sh +npm ci +npm run build +``` + +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. +The SDK comes from the locked `@csound/wasm-bin` +package. 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. + +Until the upstream browser changes ship, check out their commit and build it +with the helper. It also supplies Java through Nix for the browser compiler: + +```sh +git clone https://github.com/csound/csound.git .csound +git -C .csound checkout dfe33946847fd2fe5fe04948d9ae44ba320c5390 +npm run build:runtime +export CSOUND_BROWSER_URL="file://$PWD/.csound/platform/wasm-wasi/browser/dist/csound.js" +npm test +npm pack +``` + +You can pass an existing Csound checkout to `npm run build:runtime -- /path/to/csound`. +`CSOUND_BROWSER_URL` also accepts the absolute file URL of a built browser bundle. +Without it, tests use the locked npm browser package. 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. + +## First npm release + +After the upstream fix ships, update the browser and WASM SDK pins and lockfile, +remove the temporary runtime checkout/build from CI, and run the checks against +the published browser package. Remove `private` from `package.json`, set the +package 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 00000000..229265c2 --- /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 00000000..89b6f907 --- /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-runtime.sh b/wasm/scripts/build-runtime.sh new file mode 100644 index 00000000..f7203076 --- /dev/null +++ b/wasm/scripts/build-runtime.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/../.." + +# Temporary until the runtime changes ship in @csound/browser and @csound/wasm-bin. +csound_source=$(cd "${1:-.csound}" && pwd) +args=(-o result-browser --argstr moduleKind browser) +if [[ -n "${NIX_SYSTEM:-}" ]]; then + args+=(--argstr system "$NIX_SYSTEM") +fi +nixpkgs=$(nix-instantiate --eval --strict --json wasm/nixpkgs.nix | node -e \ + 'let input=""; process.stdin.on("data", c => input += c); process.stdin.on("end", () => console.log(JSON.parse(input)));') +nix-build "$csound_source/platform/wasm-wasi/src/csound.nix" -I "nixpkgs=$nixpkgs" "${args[@]}" + +browser_dir="$csound_source/platform/wasm-wasi/browser" +npm ci --prefix "$browser_dir" +for file in csound.wasm csound.wasm.z; do + install -m 644 "result-browser/lib/$file" "$browser_dir/node_modules/@csound/wasm-bin/lib/$file" +done +export CSOUND_BROWSER_BUILD_DIR="$browser_dir" +# Expand the exported path in the shell that Nix starts. +# shellcheck disable=SC2016 +nix-shell -I "nixpkgs=$nixpkgs" -p jdk21 --run \ + 'npm run build:prod --prefix "$CSOUND_BROWSER_BUILD_DIR"' diff --git a/wasm/scripts/build.sh b/wasm/scripts/build.sh new file mode 100644 index 00000000..22c50706 --- /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 00000000..7c990ac3 --- /dev/null +++ b/wasm/tests/runtime.test.js @@ -0,0 +1,117 @@ +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 + +`; + +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]); + }); +}); From 0ace7f0c2d063a07c296eab9437d4ad1d5be14a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Tue, 13 Oct 2026 22:22:43 +0300 Subject: [PATCH 2/5] chore(wasm): use reviewed plugin runtime --- .github/workflows/plugins_wasm.yml | 2 +- wasm/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugins_wasm.yml b/.github/workflows/plugins_wasm.yml index c9b7f3fa..7c9b9cec 100644 --- a/.github/workflows/plugins_wasm.yml +++ b/.github/workflows/plugins_wasm.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v7.0.1 with: repository: csound/csound - ref: dfe33946847fd2fe5fe04948d9ae44ba320c5390 + ref: aed14beaa392dfe9c12270abd9cd63e57651be06 path: .csound - uses: actions/setup-node@v6 with: diff --git a/wasm/README.md b/wasm/README.md index af4a19df..02d80771 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -49,7 +49,7 @@ with the helper. It also supplies Java through Nix for the browser compiler: ```sh git clone https://github.com/csound/csound.git .csound -git -C .csound checkout dfe33946847fd2fe5fe04948d9ae44ba320c5390 +git -C .csound checkout aed14beaa392dfe9c12270abd9cd63e57651be06 npm run build:runtime export CSOUND_BROWSER_URL="file://$PWD/.csound/platform/wasm-wasi/browser/dist/csound.js" npm test From e0b46747f16d7a943d2cec55825c884f618ecd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Tue, 13 Oct 2026 23:34:21 +0300 Subject: [PATCH 3/5] build(wasm): use released SDK and enable Cachix --- .github/workflows/plugins_build.yml | 4 +- .github/workflows/plugins_build_7.yml | 38 +++++------ .github/workflows/plugins_wasm.yml | 14 ++--- package-lock.json | 8 +-- package.json | 25 ++++++-- wasm/README.md | 91 ++++++++++++++++++--------- wasm/runtime.json | 3 + wasm/scripts/build-runtime.sh | 27 +++++--- wasm/shell.nix | 4 ++ 9 files changed, 137 insertions(+), 77 deletions(-) create mode 100644 wasm/runtime.json create mode 100644 wasm/shell.nix diff --git a/.github/workflows/plugins_build.yml b/.github/workflows/plugins_build.yml index afcbb88a..2f811079 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 4d2ca454..a0ec5861 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 index 7c9b9cec..9206e434 100644 --- a/.github/workflows/plugins_wasm.yml +++ b/.github/workflows/plugins_wasm.yml @@ -20,13 +20,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v7.0.1 - # csound/csound#3416 must land and ship before this package can publish. - - uses: actions/checkout@v7.0.1 - with: - repository: csound/csound - ref: aed14beaa392dfe9c12270abd9cd63e57651be06 - path: .csound - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7.1.0 with: node-version: 22 cache: npm @@ -34,17 +28,19 @@ jobs: - 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: Build the required Csound runtime + - name: Build the browser wrapper with the released engine run: | npm run build:runtime echo "CSOUND_BROWSER_URL=file://$GITHUB_WORKSPACE/.csound/platform/wasm-wasi/browser/dist/csound.js" >> "$GITHUB_ENV" - name: Test and pack run: npm pack - - uses: actions/upload-artifact@v7.0.1 + - uses: actions/upload-artifact@v7.0.2 with: name: csound-plugins-npm path: csound-plugins-*.tgz diff --git a/package-lock.json b/package-lock.json index 168efd8f..ad26568f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "LGPL-2.1-or-later", "devDependencies": { "@csound/browser": "7.0.0-beta38", - "@csound/wasm-bin": "7.0.0-beta28" + "@csound/wasm-bin": "7.0.0-beta29" }, "engines": { "node": ">=22.13.0" @@ -49,9 +49,9 @@ } }, "node_modules/@csound/wasm-bin": { - "version": "7.0.0-beta28", - "resolved": "https://registry.npmjs.org/@csound/wasm-bin/-/wasm-bin-7.0.0-beta28.tgz", - "integrity": "sha512-HjXP+1XbjiYYRRZGc3EnuBSjP6zFjhFqnR0rk8XK9XOyI/rk+35eqPk49r3PwRgyISlB+pnmHiRKMsP8In4K8w==", + "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" }, diff --git a/package.json b/package.json index 47170238..356b59bd 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,19 @@ "engines": { "node": ">=22.13.0" }, - "files": ["dist", "index.js", "index.d.ts", "LICENSE", "THIRD_PARTY.md", "wasm/README.md"], + "files": [ + "dist", + "index.js", + "index.d.ts", + "LICENSE", + "THIRD_PARTY.md", + "wasm/README.md" + ], "exports": { - ".": { "types": "./index.d.ts", "import": "./index.js" }, + ".": { + "types": "./index.d.ts", + "import": "./index.js" + }, "./dist/*": "./dist/*", "./package.json": "./package.json" }, @@ -33,7 +43,14 @@ }, "devDependencies": { "@csound/browser": "7.0.0-beta38", - "@csound/wasm-bin": "7.0.0-beta28" + "@csound/wasm-bin": "7.0.0-beta29" }, - "keywords": ["csound", "audio", "plugins", "wasm", "wasi", "webassembly"] + "keywords": [ + "csound", + "audio", + "plugins", + "wasm", + "wasi", + "webassembly" + ] } diff --git a/wasm/README.md b/wasm/README.md index 02d80771..b765fdcd 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -8,10 +8,14 @@ Csound 6 is not supported. ## Runtime dependency -This draft depends on [csound/csound#3416](https://github.com/csound/csound/pull/3416). -`@csound/browser@7.0.0-beta38` cannot load these plugins. CI builds the upstream -commit in that PR and tests against it. The package stays private until a browser -release includes those changes. +The engine and plugin SDK come from the released `@csound/wasm-bin` version in +[package-lock.json](../package-lock.json). The browser loader changes have +[merged upstream](https://github.com/csound/csound/pull/3416), but still need a +release of `@csound/browser`. + +Until then, `npm run build:runtime` fetches the wrapper source pinned in +[runtime.json](runtime.json) and builds it with the released engine. CI uses the +same helper. The package stays private until the published browser can load it. ## Use @@ -28,45 +32,74 @@ 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 Node.js 22.13 or newer, then run: +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 run build:runtime +export CSOUND_BROWSER_URL="file://$PWD/.csound/platform/wasm-wasi/browser/dist/csound.js" +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. -The SDK comes from the locked `@csound/wasm-bin` -package. 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. +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. -Until the upstream browser changes ship, check out their commit and build it -with the helper. It also supplies Java through Nix for the browser compiler: +The runtime helper supplies Java through [shell.nix](shell.nix). It builds only +the JavaScript wrapper; it does not rebuild the engine. For work on the loader, +pass an existing Csound checkout to +`npm run build:runtime -- /path/to/csound` and point `CSOUND_BROWSER_URL` at that +checkout's built `platform/wasm-wasi/browser/dist/csound.js` file. -```sh -git clone https://github.com/csound/csound.git .csound -git -C .csound checkout aed14beaa392dfe9c12270abd9cd63e57651be06 -npm run build:runtime -export CSOUND_BROWSER_URL="file://$PWD/.csound/platform/wasm-wasi/browser/dist/csound.js" -npm test -npm pack -``` +Without `CSOUND_BROWSER_URL`, tests use the locked npm browser package. 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. + +## 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. +- [runtime.json](runtime.json): the temporary browser wrapper source revision. + The helper fetches it for both local builds and CI. +- [nixpkgs.nix](nixpkgs.nix): the Nix toolchain revision and hash, shared by the + plugin build and the wrapper build shell. -You can pass an existing Csound checkout to `npm run build:runtime -- /path/to/csound`. -`CSOUND_BROWSER_URL` also accepts the absolute file URL of a built browser bundle. -Without it, tests use the locked npm browser package. 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. +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 -After the upstream fix ships, update the browser and WASM SDK pins and lockfile, -remove the temporary runtime checkout/build from CI, and run the checks against -the published browser package. Remove `private` from `package.json`, set the -package version, and run `npm publish --tag beta --access public` from a clean -build. No workflow publishes this package yet. +Once the browser loader ships, update `@csound/browser` and the lockfile, then +run the checks against that published package without `CSOUND_BROWSER_URL`. +Remove the temporary runtime build step, helper, source pin, and Java shell. +Remove `private` from `package.json`, set the package version, and run +`npm publish --tag beta --access public` from a clean build. No workflow publishes +this package yet. diff --git a/wasm/runtime.json b/wasm/runtime.json new file mode 100644 index 00000000..b90aa56e --- /dev/null +++ b/wasm/runtime.json @@ -0,0 +1,3 @@ +{ + "revision": "aed14beaa392dfe9c12270abd9cd63e57651be06" +} diff --git a/wasm/scripts/build-runtime.sh b/wasm/scripts/build-runtime.sh index f7203076..19f8ad5c 100644 --- a/wasm/scripts/build-runtime.sh +++ b/wasm/scripts/build-runtime.sh @@ -2,23 +2,30 @@ set -euo pipefail cd "$(dirname "$0")/../.." -# Temporary until the runtime changes ship in @csound/browser and @csound/wasm-bin. -csound_source=$(cd "${1:-.csound}" && pwd) -args=(-o result-browser --argstr moduleKind browser) -if [[ -n "${NIX_SYSTEM:-}" ]]; then - args+=(--argstr system "$NIX_SYSTEM") +# Temporary until the loader changes ship in @csound/browser. Use the released +# engine from this project's lockfile; only the JavaScript wrapper needs a build. +wasm_bin_dir="$PWD/node_modules/@csound/wasm-bin/lib" +if [[ $# -eq 0 ]]; then + revision=$(node -p 'require("./wasm/runtime.json").revision') + csound_source="$PWD/.csound" + if [[ ! -d "$csound_source" ]]; then + git init --quiet "$csound_source" + fi + if [[ "$(git -C "$csound_source" rev-parse HEAD 2>/dev/null || true)" != "$revision" ]]; then + git -C "$csound_source" fetch --depth 1 https://github.com/csound/csound.git "$revision" + git -C "$csound_source" checkout --detach "$revision" + fi +else + csound_source=$(cd "$1" && pwd) fi -nixpkgs=$(nix-instantiate --eval --strict --json wasm/nixpkgs.nix | node -e \ - 'let input=""; process.stdin.on("data", c => input += c); process.stdin.on("end", () => console.log(JSON.parse(input)));') -nix-build "$csound_source/platform/wasm-wasi/src/csound.nix" -I "nixpkgs=$nixpkgs" "${args[@]}" browser_dir="$csound_source/platform/wasm-wasi/browser" npm ci --prefix "$browser_dir" for file in csound.wasm csound.wasm.z; do - install -m 644 "result-browser/lib/$file" "$browser_dir/node_modules/@csound/wasm-bin/lib/$file" + install -m 644 "$wasm_bin_dir/$file" "$browser_dir/node_modules/@csound/wasm-bin/lib/$file" done export CSOUND_BROWSER_BUILD_DIR="$browser_dir" # Expand the exported path in the shell that Nix starts. # shellcheck disable=SC2016 -nix-shell -I "nixpkgs=$nixpkgs" -p jdk21 --run \ +nix-shell wasm/shell.nix --run \ 'npm run build:prod --prefix "$CSOUND_BROWSER_BUILD_DIR"' diff --git a/wasm/shell.nix b/wasm/shell.nix new file mode 100644 index 00000000..89fb8b27 --- /dev/null +++ b/wasm/shell.nix @@ -0,0 +1,4 @@ +{ pkgs ? import (import ./nixpkgs.nix) {} }: +pkgs.mkShell { + packages = [ pkgs.jdk21 ]; +} From 59771ad442b8e11be30dc3e7f7f910b66d7dec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Tue, 13 Oct 2026 23:49:07 +0300 Subject: [PATCH 4/5] build(wasm): test against published browser runtime --- .github/workflows/plugins_wasm.yml | 4 --- package-lock.json | 8 +++--- package.json | 3 +-- wasm/README.md | 42 ++++++++++-------------------- wasm/runtime.json | 3 --- wasm/scripts/build-runtime.sh | 31 ---------------------- wasm/shell.nix | 4 --- 7 files changed, 19 insertions(+), 76 deletions(-) delete mode 100644 wasm/runtime.json delete mode 100644 wasm/scripts/build-runtime.sh delete mode 100644 wasm/shell.nix diff --git a/.github/workflows/plugins_wasm.yml b/.github/workflows/plugins_wasm.yml index 9206e434..44693086 100644 --- a/.github/workflows/plugins_wasm.yml +++ b/.github/workflows/plugins_wasm.yml @@ -34,10 +34,6 @@ jobs: run: npm ci - name: Build WASM plugins with CMake and Nix run: npm run build - - name: Build the browser wrapper with the released engine - run: | - npm run build:runtime - echo "CSOUND_BROWSER_URL=file://$GITHUB_WORKSPACE/.csound/platform/wasm-wasi/browser/dist/csound.js" >> "$GITHUB_ENV" - name: Test and pack run: npm pack - uses: actions/upload-artifact@v7.0.2 diff --git a/package-lock.json b/package-lock.json index ad26568f..1f9e10f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "7.0.0-beta.0", "license": "LGPL-2.1-or-later", "devDependencies": { - "@csound/browser": "7.0.0-beta38", + "@csound/browser": "7.0.0-beta39", "@csound/wasm-bin": "7.0.0-beta29" }, "engines": { @@ -27,9 +27,9 @@ } }, "node_modules/@csound/browser": { - "version": "7.0.0-beta38", - "resolved": "https://registry.npmjs.org/@csound/browser/-/browser-7.0.0-beta38.tgz", - "integrity": "sha512-Q64BnO6VMjpPNJgP9VZzfTVKB3VAuN/zfIfqyUSFCjsY2SPw3KUqfpU+e1NxWt8VZEkNvSF23OjqCzR40kqgUg==", + "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": { diff --git a/package.json b/package.json index 356b59bd..5242717b 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,11 @@ }, "scripts": { "build": "bash wasm/scripts/build.sh", - "build:runtime": "bash wasm/scripts/build-runtime.sh", "test": "node --test wasm/tests/*.test.js", "prepack": "npm test" }, "devDependencies": { - "@csound/browser": "7.0.0-beta38", + "@csound/browser": "7.0.0-beta39", "@csound/wasm-bin": "7.0.0-beta29" }, "keywords": [ diff --git a/wasm/README.md b/wasm/README.md index b765fdcd..72ce0e80 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -8,18 +8,14 @@ Csound 6 is not supported. ## Runtime dependency -The engine and plugin SDK come from the released `@csound/wasm-bin` version in -[package-lock.json](../package-lock.json). The browser loader changes have -[merged upstream](https://github.com/csound/csound/pull/3416), but still need a -release of `@csound/browser`. - -Until then, `npm run build:runtime` fetches the wrapper source pinned in -[runtime.json](runtime.json) and builds it with the released engine. CI uses the -same helper. The package stays private until the published browser can load it. +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 -With a compatible browser build: +Use a compatible `@csound/browser` release, such as the version pinned in +[package.json](../package.json): ```js import { Csound } from "@csound/browser"; @@ -55,8 +51,6 @@ Install Nix and a Node.js version that meets [package.json](../package.json)'s ```sh npm ci npm run build -npm run build:runtime -export CSOUND_BROWSER_URL="file://$PWD/.csound/platform/wasm-wasi/browser/dist/csound.js" npm test npm pack ``` @@ -67,16 +61,11 @@ 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. -The runtime helper supplies Java through [shell.nix](shell.nix). It builds only -the JavaScript wrapper; it does not rebuild the engine. For work on the loader, -pass an existing Csound checkout to -`npm run build:runtime -- /path/to/csound` and point `CSOUND_BROWSER_URL` at that -checkout's built `platform/wasm-wasi/browser/dist/csound.js` file. - -Without `CSOUND_BROWSER_URL`, tests use the locked npm browser package. 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. +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 @@ -85,10 +74,8 @@ 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. -- [runtime.json](runtime.json): the temporary browser wrapper source revision. - The helper fetches it for both local builds and CI. - [nixpkgs.nix](nixpkgs.nix): the Nix toolchain revision and hash, shared by the - plugin build and the wrapper build shell. + 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 @@ -97,9 +84,8 @@ GitHub Actions caches. ## First npm release -Once the browser loader ships, update `@csound/browser` and the lockfile, then -run the checks against that published package without `CSOUND_BROWSER_URL`. -Remove the temporary runtime build step, helper, source pin, and Java shell. -Remove `private` from `package.json`, set the package version, and run +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/runtime.json b/wasm/runtime.json deleted file mode 100644 index b90aa56e..00000000 --- a/wasm/runtime.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "revision": "aed14beaa392dfe9c12270abd9cd63e57651be06" -} diff --git a/wasm/scripts/build-runtime.sh b/wasm/scripts/build-runtime.sh deleted file mode 100644 index 19f8ad5c..00000000 --- a/wasm/scripts/build-runtime.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "$0")/../.." - -# Temporary until the loader changes ship in @csound/browser. Use the released -# engine from this project's lockfile; only the JavaScript wrapper needs a build. -wasm_bin_dir="$PWD/node_modules/@csound/wasm-bin/lib" -if [[ $# -eq 0 ]]; then - revision=$(node -p 'require("./wasm/runtime.json").revision') - csound_source="$PWD/.csound" - if [[ ! -d "$csound_source" ]]; then - git init --quiet "$csound_source" - fi - if [[ "$(git -C "$csound_source" rev-parse HEAD 2>/dev/null || true)" != "$revision" ]]; then - git -C "$csound_source" fetch --depth 1 https://github.com/csound/csound.git "$revision" - git -C "$csound_source" checkout --detach "$revision" - fi -else - csound_source=$(cd "$1" && pwd) -fi - -browser_dir="$csound_source/platform/wasm-wasi/browser" -npm ci --prefix "$browser_dir" -for file in csound.wasm csound.wasm.z; do - install -m 644 "$wasm_bin_dir/$file" "$browser_dir/node_modules/@csound/wasm-bin/lib/$file" -done -export CSOUND_BROWSER_BUILD_DIR="$browser_dir" -# Expand the exported path in the shell that Nix starts. -# shellcheck disable=SC2016 -nix-shell wasm/shell.nix --run \ - 'npm run build:prod --prefix "$CSOUND_BROWSER_BUILD_DIR"' diff --git a/wasm/shell.nix b/wasm/shell.nix deleted file mode 100644 index 89fb8b27..00000000 --- a/wasm/shell.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ pkgs ? import (import ./nixpkgs.nix) {} }: -pkgs.mkShell { - packages = [ pkgs.jdk21 ]; -} From 70ead80f7e5de814efa13bc77dc07c38bd736007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Wed, 14 Oct 2026 00:03:46 +0300 Subject: [PATCH 5/5] ci(wasm): disable checkout credential persistence --- .github/workflows/plugins_wasm.yml | 2 ++ wasm/tests/runtime.test.js | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/plugins_wasm.yml b/.github/workflows/plugins_wasm.yml index 44693086..c783d6d5 100644 --- a/.github/workflows/plugins_wasm.yml +++ b/.github/workflows/plugins_wasm.yml @@ -20,6 +20,8 @@ jobs: 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 diff --git a/wasm/tests/runtime.test.js b/wasm/tests/runtime.test.js index 7c990ac3..783c3c99 100644 --- a/wasm/tests/runtime.test.js +++ b/wasm/tests/runtime.test.js @@ -29,6 +29,11 @@ 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;