diff --git a/README.md b/README.md index cd861fb1..40ebdaf0 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ Tagged releases with a `v` prefix are deployed to https://python-simulator.userm 1. Update the lib/micropython-microbit-v2 to the relevant hash. Make sure that its lib/micropython submodule is updated (see checkout instructions above). 2. Review the full diff for micropython-microbit-v2. In particular, note changes to: - 1. main.c, src/Makefile and mpconfigport.h all which have simulator versions that may need updates + 1. main.c, src/Makefile and mpconfigport.h, microbitfs.c, drv_radio.c all which have simulator versions that may need updates 2. the HAL, which may require implementing in the simulator 3. the filesystem, which has a JavaScript implementation. @@ -331,6 +331,26 @@ Steps for WASM debugging in Chrome: - Enable "WebAssembly Debugging: Enable DWARF support" in DevTools Experiments - DEBUG=1 make +## License + +This software is under the MIT open source license. + +[SPDX-License-Identifier: MIT](LICENSE) + +MicroPython for micro:bit is included in the build process via a submodule. + +We use dependencies via the NPM registry as specified by the package.json file under common Open Source licenses. + +Full details of each package can be found by running `license-checker`: + +```bash +$ npx license-checker --direct --summary --production +``` + +Omit the flags as desired to obtain more detail. + +A fork of libsamplerate_js to reduce bundle size is [hosted on GitHub](https://github.com/microbit-foundation/libsamplerate-js). + ## Code of Conduct Trust, partnership, simplicity and passion are our core values we live and diff --git a/lib/micropython-microbit-v2 b/lib/micropython-microbit-v2 index 8aaa36b8..c0ea61a7 160000 --- a/lib/micropython-microbit-v2 +++ b/lib/micropython-microbit-v2 @@ -1 +1 @@ -Subproject commit 8aaa36b8d50211f5902d50e050632a88358d369f +Subproject commit c0ea61a7e62e17523b0540caa7ed4b2bf801a56b diff --git a/package-lock.json b/package-lock.json index 4c8469a1..67484c9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,53 @@ "name": "@microbit-foundation/microbit-micropython-v2-simulator", "version": "0.1.0", "license": "MIT", + "dependencies": { + "@alexanderolsen/libsamplerate-js": "microbit-foundation/libsamplerate-js#v2.1.2-microbit.1" + }, "devDependencies": { + "@types/emscripten": "^1.39.10", "esbuild": "^0.14.49", "prettier": "2.6.0", "vitest": "^0.22.1" } }, + "node_modules/@alexanderolsen/libsamplerate-js": { + "version": "2.1.2-microbit.1", + "resolved": "git+ssh://git@github.com/microbit-foundation/libsamplerate-js.git#14bdda3603133d21c07cfb48974617504576a7f7", + "license": "MIT" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@types/chai": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", @@ -29,6 +70,12 @@ "@types/chai": "*" } }, + "node_modules/@types/emscripten": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", + "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", + "dev": true + }, "node_modules/@types/node": { "version": "18.7.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", @@ -456,9 +503,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -476,9 +523,9 @@ "dev": true }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -537,10 +584,16 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -564,15 +617,15 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -582,12 +635,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -626,9 +683,9 @@ } }, "node_modules/rollup": { - "version": "2.77.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", - "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -641,9 +698,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -689,15 +746,15 @@ } }, "node_modules/vite": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.0.9.tgz", - "integrity": "sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz", + "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==", "dev": true, "dependencies": { - "esbuild": "^0.14.47", - "postcss": "^8.4.16", + "esbuild": "^0.15.9", + "postcss": "^8.4.18", "resolve": "^1.22.1", - "rollup": ">=2.75.6 <2.77.0 || ~2.77.0" + "rollup": "^2.79.1" }, "bin": { "vite": "bin/vite.js" @@ -709,12 +766,17 @@ "fsevents": "~2.3.2" }, "peerDependencies": { + "@types/node": ">= 14", "less": "*", "sass": "*", "stylus": "*", + "sugarss": "*", "terser": "^5.4.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "less": { "optional": true }, @@ -724,11 +786,371 @@ "stylus": { "optional": true }, + "sugarss": { + "optional": true + }, "terser": { "optional": true } } }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/vite/node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vitest": { "version": "0.22.1", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.22.1.tgz", diff --git a/package.json b/package.json index f16851ec..2bee2d66 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,12 @@ }, "homepage": "https://github.com/microbit-foundation/micropython-microbit-v2-simulator#readme", "devDependencies": { + "@types/emscripten": "^1.39.10", "esbuild": "^0.14.49", "prettier": "2.6.0", "vitest": "^0.22.1" + }, + "dependencies": { + "@alexanderolsen/libsamplerate-js": "microbit-foundation/libsamplerate-js#v2.1.2-microbit.1" } } diff --git a/src/Makefile b/src/Makefile index 66dbdcba..863c67d7 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,6 +15,13 @@ QSTR_DEFS = $(CODAL_PORT)/qstrdefsport.h # Include py core make definitions. include $(TOP)/py/py.mk +include $(TOP)/extmod/extmod.mk + +# The micropython-lib submodule is not needed by this project, but the MicroPython +# build system requires it if FROZEN_MANIFEST is set (which it is below). To avoid +# needing to check out the micropython-lib submodule, point MPY_LIB_DIR to a dummy +# location that has a README.md file. +MPY_LIB_DIR = $(TOP) CC = emcc LD = emcc @@ -49,7 +56,7 @@ JSFLAGS += -s ASYNCIFY_STACK_SIZE=262144 JSFLAGS += -s EXIT_RUNTIME JSFLAGS += -s MODULARIZE=1 JSFLAGS += -s EXPORT_NAME=createModule -JSFLAGS += -s EXPORTED_FUNCTIONS="['_mp_js_main','_microbit_hal_audio_ready_callback','_microbit_hal_audio_speech_ready_callback','_microbit_hal_gesture_callback','_microbit_hal_level_detector_callback','_microbit_radio_rx_buffer','_mp_js_force_stop','_mp_js_request_stop']" +JSFLAGS += -s EXPORTED_FUNCTIONS="['_mp_js_main','_microbit_hal_audio_raw_ready_callback','_microbit_hal_audio_speech_ready_callback','_microbit_hal_gesture_callback','_microbit_hal_level_detector_callback','_microbit_radio_rx_buffer','_mp_js_force_stop','_mp_js_request_stop']" JSFLAGS += -s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']" --js-library jshal.js ifdef DEBUG @@ -72,6 +79,8 @@ SRC_C += $(addprefix $(CODAL_PORT)/, \ help.c \ iters.c \ microbit_accelerometer.c \ + microbit_audiorecording.c \ + microbit_audiotrack.c \ microbit_button.c \ microbit_compass.c \ microbit_display.c \ @@ -101,8 +110,8 @@ SRC_C += $(addprefix $(CODAL_PORT)/, \ modradio.c \ modspeech.c \ modthis.c \ - modutime.c \ mphalport.c \ + utils.c \ ) SRC_C += \ @@ -117,7 +126,7 @@ SRC_C += \ $(abspath $(LOCAL_LIB_DIR)/sam/debug.c) \ SRC_O += \ - lib/utils/gchelper_m3.o \ + lib/utils/gchelper_thumb2.o \ OBJ = $(PY_O) OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) @@ -138,8 +147,7 @@ all: $(MBIT_VER_FILE) $(BUILD)/micropython.js $(MBIT_VER_FILE): FORCE $(Q)mkdir -p $(HEADER_BUILD) (cd $(TOP) && $(PYTHON) py/makeversionhdr.py $(abspath $(MP_VER_FILE))) - $(PYTHON) $(TOP)/py/makeversionhdr.py $(MBIT_VER_FILE).pre - $(CAT) $(MBIT_VER_FILE).pre | $(SED) s/MICROPY_/MICROBIT_/ > $(MBIT_VER_FILE) + (cd ../lib/micropython-microbit-v2 && $(PYTHON) src/codal_port/make_microbit_version_hdr.py $(abspath $(MBIT_VER_FILE))) $(BUILD)/micropython.js: $(OBJ) jshal.js simulator-js $(ECHO) "LINK $(BUILD)/firmware.js" diff --git a/src/board/audio/index.ts b/src/board/audio/index.ts index 9bb7f5f9..1fdb67e2 100644 --- a/src/board/audio/index.ts +++ b/src/board/audio/index.ts @@ -1,19 +1,29 @@ +import { SRC } from "@alexanderolsen/libsamplerate-js/dist/src"; import { replaceBuiltinSound } from "./built-in-sounds"; import { SoundEmojiSynthesizer } from "./sound-emoji-synthesizer"; import { parseSoundEffects } from "./sound-expressions"; +import { + create as createSampleRateConverter, + ConverterType, +} from "@alexanderolsen/libsamplerate-js"; declare global { interface Window { webkitAudioContext: typeof AudioContext; + webkitOfflineAudioContext: typeof OfflineAudioContext; } } interface AudioOptions { defaultAudioCallback: () => void; + defaultResampler: SRC; speechAudioCallback: () => void; + speechResampler: SRC; + soundExpressionResampler: SRC; + recordingResampler: SRC; } -export class Audio { +export class BoardAudio { private frequency: number = 440; // You can mute the sim before it's running so we can't immediately write to the muteNode. private muted: boolean = false; @@ -21,43 +31,61 @@ export class Audio { private oscillator: OscillatorNode | undefined; private volumeNode: GainNode | undefined; private muteNode: GainNode | undefined; + private sensitivityNode: GainNode | undefined; + + private recordingResampler: SRC | undefined; default: BufferedAudio | undefined; speech: BufferedAudio | undefined; soundExpression: BufferedAudio | undefined; currentSoundExpressionCallback: undefined | (() => void); + private stopActiveRecording: (() => void) | undefined; - constructor() {} + constructor(private microphoneEl: SVGElement) {} initializeCallbacks({ defaultAudioCallback, + defaultResampler, speechAudioCallback, + speechResampler, + soundExpressionResampler, + recordingResampler, }: AudioOptions) { if (!this.context) { throw new Error("Context must be pre-created from a user event"); } + this.recordingResampler = recordingResampler; + this.muteNode = this.context.createGain(); this.muteNode.gain.setValueAtTime( this.muted ? 0 : 1, this.context.currentTime ); this.muteNode.connect(this.context.destination); + this.sensitivityNode = this.context.createGain(); + this.sensitivityNode.gain.setValueAtTime( + 0.2, // sensitivity medium level + this.context.currentTime + ); this.volumeNode = this.context.createGain(); this.volumeNode.connect(this.muteNode); this.default = new BufferedAudio( this.context, this.volumeNode, + defaultResampler, defaultAudioCallback ); this.speech = new BufferedAudio( this.context, this.volumeNode, + speechResampler, speechAudioCallback ); this.soundExpression = new BufferedAudio( this.context, this.volumeNode, + soundExpressionResampler, () => { if (this.currentSoundExpressionCallback) { this.currentSoundExpressionCallback(); @@ -67,12 +95,13 @@ export class Audio { } async createAudioContextFromUserInteraction(): Promise { + // If we set a 44.1kHz rate then we fail to connect to user media on Mac as it selects 48000 + // So we leave it at the default hoping it's most likely to match user media... + // Until there's progress on this there doesn't seem a better way: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1674892 this.context = - this.context ?? - new (window.AudioContext || window.webkitAudioContext)({ - // The highest rate is the sound expression synth. - sampleRate: 44100, - }); + this.context ?? new (window.AudioContext || window.webkitAudioContext)(); + if (this.context.state === "suspended") { return this.context.resume(); } @@ -84,21 +113,16 @@ export class Audio { this.stopSoundExpression(); }; const synth = new SoundEmojiSynthesizer(0, onDone); + this.soundExpression!.setSampleRate(synth.sampleRate); synth.play(soundEffects); const callback = () => { const source = synth.pull(); if (this.context) { - // Use createBuffer instead of new AudioBuffer to support Safari 14.0. - const target = this.context.createBuffer( - 1, - source.length, - synth.sampleRate - ); - const channel = target.getChannelData(0); + const target = new Float32Array(source.length); for (let i = 0; i < source.length; i++) { // Buffer is (0, 1023) we need to map it to (-1, 1) - channel[i] = (source[i] - 512) / 512; + target[i] = (source[i] - 512) / 512; } this.soundExpression!.writeData(target); } @@ -129,6 +153,14 @@ export class Audio { } } + setSensitivity(sensitivity: number) { + this.sensitivityNode!.gain.setValueAtTime( + // check if this is correct + sensitivity, + this.context!.currentTime + ); + } + setVolume(volume: number) { this.volumeNode!.gain.setValueAtTime( volume / 255, @@ -155,7 +187,76 @@ export class Audio { } } + isRecording(): boolean { + return !!this.stopActiveRecording; + } + + stopRecording() { + if (this.stopActiveRecording) { + this.stopActiveRecording(); + } + } + + async startRecording( + sampleRate: number, + samplesNeeded: number, + onChunk: (chunk: Float32Array) => void + ) { + let samplesSent = 0; + if (!navigator?.mediaDevices?.getUserMedia) { + return; + } + this.stopRecording(); + + this.stopActiveRecording = () => {}; + let micStream: MediaStream | undefined; + try { + micStream = await navigator.mediaDevices.getUserMedia({ + video: false, + // It seems Firefox ignores the rate set here + audio: true, + }); + } catch (e) { + console.error(e); + this.stopRecording(); + return; + } + this.microphoneEl.style.display = "unset"; + + const source = this.context!.createMediaStreamSource(micStream); + source.connect(this.sensitivityNode!); + + const recorder = this.context!.createScriptProcessor(2048, 1, 1); + + const inputSampleRate = this.context!.sampleRate; + this.recordingResampler!.inputSampleRate = inputSampleRate; + this.recordingResampler!.outputSampleRate = sampleRate; + + recorder.onaudioprocess = (e) => { + const resampled = this.recordingResampler!.full( + e.inputBuffer.getChannelData(0) + ); + onChunk(resampled); + samplesSent += resampled.length; + if (samplesSent >= samplesNeeded) { + this.stopRecording(); + } + }; + this.stopActiveRecording = () => { + recorder.disconnect(); + this.sensitivityNode!.disconnect(); + source.disconnect(); + micStream?.getTracks().forEach((track) => track.stop()); + this.microphoneEl.style.display = "none"; + this.stopActiveRecording = undefined; + }; + + this.sensitivityNode!.connect(recorder); + recorder.connect(this.context!.destination); + } + boardStopped() { + this.stopRecording(); this.stopOscillator(); this.speech?.dispose(); this.soundExpression?.dispose(); @@ -172,35 +273,43 @@ export class Audio { class BufferedAudio { nextStartTime: number = -1; - private sampleRate: number = -1; constructor( private context: AudioContext, private destination: AudioNode, + private resampler: SRC, private callback: () => void - ) {} + ) { + this.resampler.outputSampleRate = this.context.sampleRate; + } init(sampleRate: number) { - this.sampleRate = sampleRate; - this.nextStartTime = -1; + // This is called for each new audio source so don't reset nextStartTime + // or we start to overlap audio + this.setSampleRate(sampleRate); } - createBuffer(length: number) { - // Use createBuffer instead of new AudioBuffer to support Safari 14.0. - return this.context.createBuffer(1, length, this.sampleRate); + setSampleRate(sampleRate: number) { + this.resampler.inputSampleRate = sampleRate; } - writeData(buffer: AudioBuffer) { - // Use createBufferSource instead of new AudioBufferSourceNode to support Safari 14.0. + writeData(data: Float32Array) { + // In practice the supported range is less than the 8k..96k required by the spec and varies by browser + // for a consistent performance profile we're always resampling for now rather than letting Web Audio do it + let sampleRate = this.context.sampleRate; + data = this.resampler.full(data); + + // Use createXXX instead to support Safari 14.0. + const buffer = this.context.createBuffer(1, data.length, sampleRate); + buffer.copyToChannel(data, 0); const source = this.context.createBufferSource(); source.buffer = buffer; - source.onended = this.callback; + source.onended = this.callCallback; source.connect(this.destination); const currentTime = this.context.currentTime; const first = this.nextStartTime < currentTime; const startTime = first ? currentTime : this.nextStartTime; this.nextStartTime = startTime + buffer.length / buffer.sampleRate; - // For audio frames, we're frequently out of data. Speech is smooth. if (first) { // We're just getting started so buffer another frame. this.callback(); @@ -208,6 +317,11 @@ class BufferedAudio { source.start(startTime); } + private callCallback = () => { + // Indirect so we can clear callback later + this.callback(); + }; + dispose() { // Prevent calls into WASM when the buffer nodes finish. this.callback = () => {}; diff --git a/src/board/conversions.ts b/src/board/conversions.ts index 392d7808..f42bca7f 100644 --- a/src/board/conversions.ts +++ b/src/board/conversions.ts @@ -98,12 +98,11 @@ export function convertAccelerometerNumberToString(value: number): string { export const convertAudioBuffer = ( heap: Uint8Array, source: number, - target: AudioBuffer + target: Float32Array ) => { - const channel = target.getChannelData(0); - for (let i = 0; i < channel.length; ++i) { + for (let i = 0; i < target.length; ++i) { // Convert from uint8 to -1..+1 float. - channel[i] = (heap[source + i] / 255) * 2 - 1; + target[i] = (heap[source + i] / 255) * 2 - 1; } return target; }; diff --git a/src/board/index.ts b/src/board/index.ts index deff7fa7..e3752c20 100644 --- a/src/board/index.ts +++ b/src/board/index.ts @@ -1,6 +1,7 @@ +import { create as createResampler } from "@alexanderolsen/libsamplerate-js"; import svgText from "../microbit-drawing.svg"; import { Accelerometer } from "./accelerometer"; -import { Audio } from "./audio"; +import { BoardAudio } from "./audio"; import { Button } from "./buttons"; import { Compass } from "./compass"; import { @@ -92,7 +93,7 @@ export class Board { display: Display; buttons: Button[]; pins: Pin[]; - audio: Audio; + audio: BoardAudio; temperature: RangeSensor; microphone: Microphone; accelerometer: Accelerometer; @@ -202,7 +203,7 @@ export class Board { this.pins[MICROBIT_HAL_PIN_P19] = new StubPin("pin19"); this.pins[MICROBIT_HAL_PIN_P20] = new StubPin("pin20"); - this.audio = new Audio(); + this.audio = new BoardAudio(this.svg.querySelector("#LitMicrophone")!); this.temperature = new RangeSensor("temperature", -5, 50, 21, "°C"); this.accelerometer = new Accelerometer(onChange); this.compass = new Compass(); @@ -247,10 +248,22 @@ export class Board { noInitialRun: true, instantiateWasm, }); + + // We update the sample rates before use. + const recordingResampler = await createResampler(1, 48000, 48000); + const defaultResampler = await createResampler(1, 48000, 48000); + const speechResampler = await createResampler(1, 48000, 48000); + // Probably this one is never used so would be nice to avoid + const soundExpressionResampler = await createResampler(1, 48000, 48000); + const module = new ModuleWrapper(wrapped); this.audio.initializeCallbacks({ - defaultAudioCallback: wrapped._microbit_hal_audio_ready_callback, + defaultAudioCallback: wrapped._microbit_hal_audio_raw_ready_callback, + defaultResampler, speechAudioCallback: wrapped._microbit_hal_audio_speech_ready_callback, + speechResampler, + soundExpressionResampler, + recordingResampler, }); this.accelerometer.initializeCallbacks( wrapped._microbit_hal_gesture_callback diff --git a/src/board/pins.ts b/src/board/pins.ts index da5d4435..c9980f7c 100644 --- a/src/board/pins.ts +++ b/src/board/pins.ts @@ -9,6 +9,8 @@ export interface Pin { isTouched(): boolean; + getAndClearTouches(): number; + boardStopped(): void; setAnalogPeriodUs(period: number): number; @@ -44,6 +46,10 @@ abstract class BasePin implements Pin { return this.analogPeriodUs; } + getAndClearTouches(): number { + return 0; + } + isTouched(): boolean { return false; } @@ -58,6 +64,8 @@ export class StubPin extends BasePin {} export class TouchPin extends BasePin { private _mouseDown: boolean = false; + private _touches: number = 0; + private keyListener: (e: KeyboardEvent) => void; private mouseDownListener: (e: MouseEvent) => void; private touchStartListener: (e: TouchEvent) => void; @@ -128,8 +136,20 @@ export class TouchPin extends BasePin { this.setValueInternal(value, false); } + getAndClearTouches() { + const touches = this._touches; + this._touches = 0; + return touches; + } + private setValueInternal(value: any, internalChange: boolean) { + const previous = this.state.value; super.setValue(value); + // If this value is transitioning from high to low then count a touch. + // Do it after setValue because the input can be converted from a string. + if (previous === this.state.max && this.state.value === this.state.min) { + this._touches++; + } if (internalChange) { this.onChange({ @@ -179,5 +199,7 @@ export class TouchPin extends BasePin { } } - boardStopped() {} + boardStopped() { + this._touches = 0; + } } diff --git a/src/board/wasm.ts b/src/board/wasm.ts index 6d5bbfbf..77eddec4 100644 --- a/src/board/wasm.ts +++ b/src/board/wasm.ts @@ -2,21 +2,19 @@ import { Board } from "."; import * as conversions from "./conversions"; import { FileSystem } from "./fs"; -export interface EmscriptenModule { +export interface SimulatorEmscriptenModule extends EmscriptenModule { cwrap: any; ExitStatus: Error; // See EXPORTED_FUNCTIONS in the Makefile. _mp_js_request_stop(): void; _mp_js_force_stop(): void; - _microbit_hal_audio_ready_callback(): void; + _microbit_hal_audio_raw_ready_callback(): void; _microbit_hal_audio_speech_ready_callback(): void; _microbit_hal_gesture_callback(gesture: number): void; _microbit_hal_level_detector_callback(level: number): void; _microbit_radio_rx_buffer(): number; - HEAPU8: Uint8Array; - // Added by us at module creation time for jshal to access. board: Board; fs: FileSystem; @@ -26,7 +24,7 @@ export interface EmscriptenModule { export class ModuleWrapper { private main: () => Promise; - constructor(private module: EmscriptenModule) { + constructor(private module: SimulatorEmscriptenModule) { const main = module.cwrap("mp_js_main", "null", ["number"], { async: true, }); diff --git a/src/demo.html b/src/demo.html index 83dabe83..8f3cd3f9 100644 --- a/src/demo.html +++ b/src/demo.html @@ -90,10 +90,19 @@

MicroPython-micro:bit simulator example embedding

+ + + + +