From f33ec0e2afdbb9cb78258e9273dd4d0db2f47414 Mon Sep 17 00:00:00 2001 From: nkymut Date: Thu, 21 Nov 2024 12:01:17 +0800 Subject: [PATCH 1/9] Add Device Motion package Introduces smart-phone device's acceleration, gravity, rotation, and orientation signals for dynamic pattern creation in Strudel. --- packages/motion/README.md | 66 ++++++ packages/motion/index.mjs | 3 + packages/motion/motion.mjs | 366 +++++++++++++++++++++++++++++++++ packages/motion/package.json | 38 ++++ packages/motion/vite.config.js | 19 ++ website/package.json | 3 +- website/src/repl/util.mjs | 1 + 7 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 packages/motion/README.md create mode 100644 packages/motion/index.mjs create mode 100644 packages/motion/motion.mjs create mode 100644 packages/motion/package.json create mode 100644 packages/motion/vite.config.js diff --git a/packages/motion/README.md b/packages/motion/README.md new file mode 100644 index 000000000..5874b820f --- /dev/null +++ b/packages/motion/README.md @@ -0,0 +1,66 @@ +# @strudel/motion + +This package adds device motion sensing functionality to strudel Patterns. + +## Install + +```sh +npm i @strudel/motion --save +``` + +## Setup SSL for Local Development +`DeviceMotionEvent` only work over HTTPS, so you'll need to set up SSL for local development. +install SSL plugin for Vite +`pnpm install -D @vitejs/plugin-basic-ssl` + +add the basicSsl plugin to the defineConfig block in `strudel/website/astro.config.mjs` +``` +vite: { + plugins: [basicSsl()], + server: { + host: '0.0.0.0', // Ensures it binds to all network interfaces + // https: { + // key: '../../key.pem', // + // cert: '../../cert.pem', + // }, + }, +}, +``` + +generate SSL cert if its necessary + +`openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout key.pem -out cert.pem` + +## Usage + +| Motion | Long Names & Aliases | Description | +|----------------------------|-----------------------------------------------------------|------------------------------------------| +| Acceleration | accelerationX (accX), accelerationY (accY), accelerationZ (accZ) | X, Y, Z-axis acceleration values | +| Gravity | gravityX (gravX), gravityY (gravY), gravityZ (gravZ) | X, Y, Z-axis gravity values | +| Rotation | rotationAlpha (rotA, rotZ), rotationBeta (rotB, rotX), rotationGamma (rotG, rotY) | Rotation around alpha, beta, gamma axes and mapped to X, Y, Z | +| Orientation | orientationAlpha (oriA, oriZ), orientationBeta (oriB, oriX), orientationGamma (oriG, oriY) | Orientation alpha, beta, gamma values and mapped to X, Y, Z | +| Absolute Orientation | absoluteOrientationAlpha (absOriA, absOriZ), absoluteOrientationBeta (absOriB, absOriX), absoluteOrientationGamma (absOriG, absOriY) | Absolute orientation alpha, beta, gamma values and mapped to X, Y, Z | + +## Example + +``` +enableMotion() //enable DeviceMotion + +let tempo = 200 + +$_: accX.segment(16).gain().log() + +$:n("[0 1 3 1 5 4]/4") + .scale("Bb:lydian") + .sometimesBy(0.5,sub(note(12))) + .lpf(gravityY.range(20,1000)) + .lpq(gravityZ.range(1,30)) + .lpenv(gravityX.range(2,2)) + .gain(oriX.range(0.2,0.8)) + .room(oriZ.range(0,0.5)) + .attack(oriY.range(0,0.3)) + .delay(rotG.range(0,1)) + .decay(rotA.range(0,1)) + .attack(rotB.range(0,0.1)) + .sound("sawtooth").cpm(tempo) +``` \ No newline at end of file diff --git a/packages/motion/index.mjs b/packages/motion/index.mjs new file mode 100644 index 000000000..b315b6173 --- /dev/null +++ b/packages/motion/index.mjs @@ -0,0 +1,3 @@ +import './motion.mjs'; + +export * from './motion.mjs'; diff --git a/packages/motion/motion.mjs b/packages/motion/motion.mjs new file mode 100644 index 000000000..f0a4fb419 --- /dev/null +++ b/packages/motion/motion.mjs @@ -0,0 +1,366 @@ +// motion.mjs + +import { signal } from '../core/signal.mjs'; + +/** + * The accelerometer's x-axis value ranges from 0 to 1. + * @name accelerationX + * @return {Pattern} + * @synonyms accX + * @example + * n(accelerationX.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The accelerometer's y-axis value ranges from 0 to 1. + * @name accelerationY + * @return {Pattern} + * @synonyms accY + * @example + * n(accelerationY.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The accelerometer's z-axis value ranges from 0 to 1. + * @name accelerationZ + * @return {Pattern} + * @synonyms accZ + * @example + * n(accelerationZ.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's gravity x-axis value ranges from 0 to 1. + * @name gravityX + * @return {Pattern} + * @synonyms gravX + * @example + * n(gravityX.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's gravity y-axis value ranges from 0 to 1. + * @name gravityY + * @return {Pattern} + * @synonyms gravY + * @example + * n(gravityY.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's gravity z-axis value ranges from 0 to 1. + * @name gravityZ + * @return {Pattern} + * @synonyms gravZ + * @example + * n(gravityZ.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's rotation around the alpha-axis value ranges from 0 to 1. + * @name rotationAlpha + * @return {Pattern} + * @synonyms rotA, rotZ, rotationZ + * @example + * n(rotationAlpha.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's rotation around the beta-axis value ranges from 0 to 1. + * @name rotationBeta + * @return {Pattern} + * @synonyms rotB, rotX, rotationX + * @example + * n(rotationBeta.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's rotation around the gamma-axis value ranges from 0 to 1. + * @name rotationGamma + * @return {Pattern} + * @synonyms rotG, rotY, rotationY + * @example + * n(rotationGamma.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's orientation alpha value ranges from 0 to 1. + * @name orientationAlpha + * @return {Pattern} + * @synonyms oriA, oriZ, orientationZ + * @example + * n(orientationAlpha.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's orientation beta value ranges from 0 to 1. + * @name orientationBeta + * @return {Pattern} + * @synonyms oriB, oriX, orientationX + * @example + * n(orientationBeta.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's orientation gamma value ranges from 0 to 1. + * @name orientationGamma + * @return {Pattern} + * @synonyms oriG, oriY, orientationY + * @example + * n(orientationGamma.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's absolute orientation alpha value ranges from 0 to 1. + * @name absoluteOrientationAlpha + * @return {Pattern} + * @synonyms absOriA, absOriZ, absoluteOrientationZ + * @example + * n(absoluteOrientationAlpha.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's absolute orientation beta value ranges from 0 to 1. + * @name absoluteOrientationBeta + * @return {Pattern} + * @synonyms absOriB, absOriX, absoluteOrientationX + * @example + * n(absoluteOrientationBeta.segment(4).range(0,7)).scale("C:minor") + * + */ + +/** + * The device's absolute orientation gamma value ranges from 0 to 1. + * @name absoluteOrientationGamma + * @return {Pattern} + * @synonyms absOriG, absOriY, absoluteOrientationY + * @example + * n(absoluteOrientationGamma.segment(4).range(0,7)).scale("C:minor") + * + */ + +class DeviceMotionHandler { + constructor() { + this.GRAVITY = 9.81; + + // Initialize sensor values + this._acceleration = { + x: 0, + y: 0, + z: 0, + }; + + this._gravity = { + x: 0, + y: 0, + z: 0, + }; + + this._rotation = { + alpha: 0, + beta: 0, + gamma: 0, + }; + + this._orientation = { + alpha: 0, + beta: 0, + gamma: 0, + }; + + this._absoluteOrientation = { + alpha: 0, + beta: 0, + gamma: 0, + }; + + this._permissionStatus = 'unknown'; + } + + async requestPermissions() { + if (typeof DeviceMotionEvent?.requestPermission === 'function') { + try { + // iOS requires explicit permission + const motionPermission = await DeviceMotionEvent.requestPermission(); + const orientationPermission = await DeviceOrientationEvent.requestPermission(); + + this._permissionStatus = + motionPermission === 'granted' && orientationPermission === 'granted' ? 'granted' : 'denied'; + this.setupEventListeners(); + } catch (error) { + console.error('Permission request failed:', error); + this._permissionStatus = 'denied'; + } + } else { + this._permissionStatus = 'granted'; + this.setupEventListeners(); + } + } + + setupEventListeners() { + if (this._permissionStatus === 'granted') { + // Device Motion handler + window.addEventListener('devicemotion', this.handleDeviceMotion.bind(this), true); + window.addEventListener('deviceorientation', this.handleDeviceOrientation.bind(this), true); + window.addEventListener('deviceorientationabsolute', this.handleAbsoluteDeviceOrientation.bind(this), true); + } + } + + handleDeviceMotion(event) { + //console.log(event); + if (event.acceleration) { + // Normalize acceleration values to 0-1 range + this._acceleration.x = (event.acceleration.x + 1) / 2; + this._acceleration.y = (event.acceleration.y + 1) / 2; + this._acceleration.z = (event.acceleration.z + 1) / 2; + } + + if (event.accelerationIncludingGravity) { + // Normalize acceleration values to 0-1 range + this._gravity.x = (event.accelerationIncludingGravity.x + this.GRAVITY) / (2 * this.GRAVITY); + this._gravity.y = (event.accelerationIncludingGravity.y + this.GRAVITY) / (2 * this.GRAVITY); + this._gravity.z = (event.accelerationIncludingGravity.z + this.GRAVITY) / (2 * this.GRAVITY); + } + + if (event.rotationRate) { + // Normalize rotation values to 0-1 range + this._rotation.alpha = (event.rotationRate.alpha + 180) / 360; + this._rotation.beta = (event.rotationRate.beta + 180) / 360; + this._rotation.gamma = (event.rotationRate.gamma + 180) / 360; + } + } + + handleDeviceOrientation(event) { + this._orientation.alpha = event.alpha / 360; //a(0~360) + this._orientation.beta = (event.beta + 180) / 360; //b(-180~180) + this._orientation.gamma = (event.gamma + 90) / 180; //g(-90~90) + } + + handleAbsoluteDeviceOrientation(event) { + this._absoluteOrientation.alpha = event.alpha / 360; //a(0~360) + this._absoluteOrientation.beta = (event.beta + 180) / 360; //b(-180~180) + this._absoluteOrientation.gamma = (event.gamma + 90) / 180; //g(-90~90) + } + + // Getter methods for current values + getAcceleration() { + return this._acceleration; + } + getGravity() { + return this._gravity; + } + getRotation() { + return this._rotation; + } + getOrientation() { + return this._orientation; + } + getAbsoluteOrientation() { + return this._absoluteOrientation; + } +} + +// Create singleton instance +const deviceMotion = new DeviceMotionHandler(); + +// Export a function to request permission +export async function enableMotion() { + return deviceMotion.requestPermissions(); +} + +// Create signals for acceleration +export const accelerationX = signal(() => deviceMotion.getAcceleration().x); +export const accelerationY = signal(() => deviceMotion.getAcceleration().y); +export const accelerationZ = signal(() => deviceMotion.getAcceleration().z); + +// Aliases for shorter names +export const accX = accelerationX; +export const accY = accelerationY; +export const accZ = accelerationZ; + +// Create signals for gravity +export const gravityX = signal(() => deviceMotion.getGravity().x); +export const gravityY = signal(() => deviceMotion.getGravity().y); +export const gravityZ = signal(() => deviceMotion.getGravity().z); + +// Aliases for shorter names +export const gravX = gravityX; +export const gravY = gravityY; +export const gravZ = gravityZ; + +// Create signals for orientation +export const orientationAlpha = signal(() => deviceMotion.getOrientation().alpha); +export const orientationBeta = signal(() => deviceMotion.getOrientation().beta); +export const orientationGamma = signal(() => deviceMotion.getOrientation().gamma); +// Aliases for shorter names +export const orientationA = orientationAlpha; +export const orientationB = orientationBeta; +export const orientationG = orientationGamma; + +// Aliases mapping to X,Y,Z coordinates +export const orientationX = orientationBeta; +export const orientationY = orientationGamma; +export const orientationZ = orientationAlpha; + +// Short aliases for X,Y,Z +export const oriX = orientationX; +export const oriY = orientationY; +export const oriZ = orientationZ; + +// Create signals for absolute orientation +export const absoluteOrientationAlpha = signal(() => deviceMotion.getAbsoluteOrientation().alpha); +export const absoluteOrientationBeta = signal(() => deviceMotion.getAbsoluteOrientation().beta); +export const absoluteOrientationGamma = signal(() => deviceMotion.getAbsoluteOrientation().gamma); + +// Aliases for shorter names +export const absOriA = absoluteOrientationAlpha; +export const absOriB = absoluteOrientationBeta; +export const absOriG = absoluteOrientationGamma; + +// Aliases mapping to X,Y,Z coordinates +export const absoluteOrientationX = absoluteOrientationBeta; +export const absoluteOrientationY = absoluteOrientationGamma; +export const absoluteOrientationZ = absoluteOrientationAlpha; + +// Short aliases for X,Y,Z +export const absOriX = absoluteOrientationX; +export const absOriY = absoluteOrientationY; +export const absOriZ = absoluteOrientationZ; + +// Create signals for rotation +export const rotationAlpha = signal(() => deviceMotion.getRotation().alpha); +export const rotationBeta = signal(() => deviceMotion.getRotation().beta); +export const rotationGamma = signal(() => deviceMotion.getRotation().gamma); +export const rotationX = rotationBeta; +export const rotationY = rotationGamma; +export const rotationZ = rotationAlpha; + +// Aliases for shorter names +export const rotA = rotationAlpha; +export const rotB = rotationBeta; +export const rotG = rotationGamma; +export const rotX = rotationX; +export const rotY = rotationY; +export const rotZ = rotationZ; + +// // Bipolar versions (ranging from -1 to 1 instead of 0 to 1) +// export const accX2 = accX.toBipolar(); +// export const accY2 = accY.toBipolar(); +// export const accZ2 = accZ.toBipolar(); + +// export const rotA2 = rotA.toBipolar(); +// export const rotB2 = rotB.toBipolar(); +// export const rotG2 = rotG.toBipolar(); diff --git a/packages/motion/package.json b/packages/motion/package.json new file mode 100644 index 000000000..b0308a355 --- /dev/null +++ b/packages/motion/package.json @@ -0,0 +1,38 @@ +{ + "name": "@strudel/motion", + "version": "1.1.0", + "description": "DeviceMotion API for strudel", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "titdalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Yuta Nakayama ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "dependencies": { + "@strudel/core": "workspace:*" + }, + "devDependencies": { + "vite": "^5.0.10" + } +} + \ No newline at end of file diff --git a/packages/motion/vite.config.js b/packages/motion/vite.config.js new file mode 100644 index 000000000..5df3edc1b --- /dev/null +++ b/packages/motion/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'index.mjs'), + formats: ['es'], + fileName: (ext) => ({ es: 'index.mjs' })[ext], + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/website/package.json b/website/package.json index 942fee9c6..2e22c1dbb 100644 --- a/website/package.json +++ b/website/package.json @@ -33,14 +33,15 @@ "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", + "@strudel/motion": "workspace:*", "@strudel/osc": "workspace:*", "@strudel/serial": "workspace:*", "@strudel/soundfonts": "workspace:*", + "@strudel/tidal": "workspace:*", "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@strudel/webaudio": "workspace:*", "@strudel/xen": "workspace:*", - "@strudel/tidal": "workspace:*", "@supabase/supabase-js": "^2.39.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 905e16b09..7eede36ea 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -81,6 +81,7 @@ export function loadModules() { import('@strudel/soundfonts'), import('@strudel/csound'), import('@strudel/tidal'), + import('@strudel/motion'), ]; if (isTauri()) { modules = modules.concat([ From a5b05d211e9f5fbcd8e3769b73c3621c4699c46c Mon Sep 17 00:00:00 2001 From: nkymut Date: Tue, 17 Dec 2024 17:28:53 +0800 Subject: [PATCH 2/9] Add DOC for devicemotion --- packages/motion/docs/devicemotion.mdx | 82 ++++++++++++++++++++++++ packages/motion/motion.mjs | 7 +- test/examples.test.mjs | 15 ----- website/src/config.ts | 1 + website/src/pages/learn/devicemotion.mdx | 10 +++ 5 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 packages/motion/docs/devicemotion.mdx delete mode 100644 test/examples.test.mjs create mode 100644 website/src/pages/learn/devicemotion.mdx diff --git a/packages/motion/docs/devicemotion.mdx b/packages/motion/docs/devicemotion.mdx new file mode 100644 index 000000000..ebf1dbc4a --- /dev/null +++ b/packages/motion/docs/devicemotion.mdx @@ -0,0 +1,82 @@ +import { MiniRepl } from '../../../website/src/docs/MiniRepl'; +import { JsDoc } from '../../../website/src/docs/JsDoc'; + +# Device Motion + +Devicemotion module allows you to use your mobile device's motion sensors (accelerometer, gyroscope, and orientation sensors) to control musical parameters in real-time. This creates opportunities for expressive, movement-based musical interactions. + +## Basic Setup + +First, you need to enable device motion sensing: + + + +This will prompt the user for permission to access device motion sensors. + +## Available Motion Parameters + +You can access different types of motion data: + +| Motion | Long Names & Aliases | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| Acceleration | accelerationX (accX), accelerationY (accY), accelerationZ (accZ) | Measures linear acceleration of the device, excluding gravity. Raw values are normalized from g-force. | +| Gravity | gravityX (gravX), gravityY (gravY), gravityZ (gravZ) | Indicates device's orientation relative to Earth's gravity. Raw values are normalized from ±9.81 m/s². | +| Rotation | rotationAlpha (rotA, rotZ), rotationBeta (rotB, rotX), rotationGamma (rotG, rotY) | Measures rotation rate around each axis. Raw values (±180°/s) are normalized. | +| Orientation | orientationAlpha (oriA, oriZ), orientationBeta (oriB, oriX), orientationGamma (oriG, oriY) | Relative orientation from its starting device position. Normalized from:
- Alpha: 0° to 360°
- Beta: -180° to 180°
- Gamma: -90° to 90° | +| Absolute Orientation | absoluteOrientationAlpha (absOriA, absOriZ), absoluteOrientationBeta (absOriB, absOriX), absoluteOrientationGamma (absOriG, absOriY) | **Not available for iOS**
Earth-referenced orientation using magnetometer. Same normalization as Orientation. | + +Note: + +- All motion values are normalized to a range of 0 to 1. +- Not all devices have the same sensors available + Check [DeviceMotionEvent API](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent) for browser compatibility +- Refer to [Oritentation and motion data explained](https://developer.mozilla.org/en-US/docs/Web/API/Device_orientation_events/Orientation_and_motion_data_explained) for more details + +### Orientation vs Absolute Orientation + +The key difference between regular orientation and absolute orientation is: + +- Regular orientation (`oriX/Y/Z`) measures relative changes in device orientation from its starting position +- Absolute orientation (`absOriX/Y/Z`) measures orientation relative to Earth's magnetic field and gravity, providing consistent absolute values regardless of starting position + +For example, if you rotate your device 90 degrees clockwise and then back: + +- Regular orientation will show a change during rotation but return to initial values +- Absolute orientation will show the actual compass heading throughout + +This makes absolute orientation particularly useful for creating direction-based musical interactions - for example, performers facing north could play one melody while those facing south play another, creating spatially-aware ensemble performances. Regular orientation, on the other hand, is better suited for detecting relative motion and gestures regardless of which direction the performer is facing. + +## Basic Example + +Here's a simple example that uses device motion to control a synthesizer: + + + +## Tips for Using Motion Controls + +1. Use `.range(min, max)` to map sensor values to musically useful ranges +2. Consider using `.segment()` to smooth out rapid changes in sensor values + +## Debugging + +You can use `segment(16).log()` to see the raw values from any motion sensor: + +```javascript +$_: accX.segment(16).log(); // logs acceleration values to the console +``` + +This is helpful when calibrating your ranges and understanding how your device responds to different movements. + +Remember that device motion works best on mobile devices and may not be available on all desktop browsers. Always test your motion-controlled pieces on the target device type! diff --git a/packages/motion/motion.mjs b/packages/motion/motion.mjs index f0a4fb419..875704eae 100644 --- a/packages/motion/motion.mjs +++ b/packages/motion/motion.mjs @@ -315,7 +315,12 @@ export const orientationX = orientationBeta; export const orientationY = orientationGamma; export const orientationZ = orientationAlpha; -// Short aliases for X,Y,Z +// Short aliases for A,B,G,X,Y,Z + +export const oriA = orientationAlpha; +export const oriB = orientationBeta; +export const oriG = orientationGamma; + export const oriX = orientationX; export const oriY = orientationY; export const oriZ = orientationZ; diff --git a/test/examples.test.mjs b/test/examples.test.mjs deleted file mode 100644 index 44ffdd5a9..000000000 --- a/test/examples.test.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { queryCode } from './runtime.mjs'; -import { describe, it } from 'vitest'; -import doc from '../doc.json'; - -describe('runs examples', () => { - const { docs } = doc; - docs.forEach(async (doc) => { - doc.examples?.forEach((example, i) => { - it(`example "${doc.name}" example index ${i}`, async ({ expect }) => { - const haps = await queryCode(example, 4); - expect(haps).toMatchSnapshot(); - }); - }); - }); -}); diff --git a/website/src/config.ts b/website/src/config.ts index dd003c185..918c7b731 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -84,6 +84,7 @@ export const SIDEBAR: Sidebar = { { text: 'Music metadata', link: 'learn/metadata' }, { text: 'CSound', link: 'learn/csound' }, { text: 'Hydra', link: 'learn/hydra' }, + { text: 'Device Motion', link: 'learn/devicemotion' }, ], 'Pattern Functions': [ { text: 'Introduction', link: 'functions/intro' }, diff --git a/website/src/pages/learn/devicemotion.mdx b/website/src/pages/learn/devicemotion.mdx new file mode 100644 index 000000000..cd54a7cf8 --- /dev/null +++ b/website/src/pages/learn/devicemotion.mdx @@ -0,0 +1,10 @@ +--- +title: Device Motion +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; +import DeviceMotion from '../../../../packages/motion/docs/devicemotion.mdx'; + + From cdf50cbaab17e9371355cc03492065dc5d6bec54 Mon Sep 17 00:00:00 2001 From: Yuta Nakayama Date: Sat, 18 Jan 2025 07:09:43 +0800 Subject: [PATCH 3/9] Update README.md minor edit on SSL installation procedure --- packages/motion/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/motion/README.md b/packages/motion/README.md index 5874b820f..81ff53935 100644 --- a/packages/motion/README.md +++ b/packages/motion/README.md @@ -9,9 +9,13 @@ npm i @strudel/motion --save ``` ## Setup SSL for Local Development -`DeviceMotionEvent` only work over HTTPS, so you'll need to set up SSL for local development. -install SSL plugin for Vite -`pnpm install -D @vitejs/plugin-basic-ssl` +`DeviceMotionEvent` only works with HTTPS, so you'll need to enable SSL for local development. +Try installing an SSL plugin for Vite. + +``` +cd website +pnpm install -D @vitejs/plugin-basic-ssl +``` add the basicSsl plugin to the defineConfig block in `strudel/website/astro.config.mjs` ``` @@ -27,7 +31,7 @@ vite: { }, ``` -generate SSL cert if its necessary +generate an SSL certificate to avoid security warnings. `openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout key.pem -out cert.pem` @@ -63,4 +67,4 @@ $:n("[0 1 3 1 5 4]/4") .decay(rotA.range(0,1)) .attack(rotB.range(0,0.1)) .sound("sawtooth").cpm(tempo) -``` \ No newline at end of file +``` From bcef67e5a1775506cd03def084701d5249f91786 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 31 Jan 2025 08:19:26 +0100 Subject: [PATCH 4/9] chore: bump vite + update lockfile --- packages/motion/package.json | 2 +- pnpm-lock.yaml | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/motion/package.json b/packages/motion/package.json index b0308a355..cc117a268 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -32,7 +32,7 @@ "@strudel/core": "workspace:*" }, "devDependencies": { - "vite": "^5.0.10" + "vite": "^6.0.11" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f85e3ab6e..494d04f0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,16 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/motion: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + packages/mqtt: dependencies: '@strudel/core': @@ -639,6 +649,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/motion': + specifier: workspace:* + version: link:../packages/motion '@strudel/mqtt': specifier: workspace:* version: link:../packages/mqtt From 44828e20479e24bb6e91d81ec8d586542b7c343c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 31 Jan 2025 08:22:04 +0100 Subject: [PATCH 5/9] docs: move dev info down --- packages/motion/README.md | 56 ++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/motion/README.md b/packages/motion/README.md index 81ff53935..f122a675e 100644 --- a/packages/motion/README.md +++ b/packages/motion/README.md @@ -8,33 +8,6 @@ This package adds device motion sensing functionality to strudel Patterns. npm i @strudel/motion --save ``` -## Setup SSL for Local Development -`DeviceMotionEvent` only works with HTTPS, so you'll need to enable SSL for local development. -Try installing an SSL plugin for Vite. - -``` -cd website -pnpm install -D @vitejs/plugin-basic-ssl -``` - -add the basicSsl plugin to the defineConfig block in `strudel/website/astro.config.mjs` -``` -vite: { - plugins: [basicSsl()], - server: { - host: '0.0.0.0', // Ensures it binds to all network interfaces - // https: { - // key: '../../key.pem', // - // cert: '../../cert.pem', - // }, - }, -}, -``` - -generate an SSL certificate to avoid security warnings. - -`openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout key.pem -out cert.pem` - ## Usage | Motion | Long Names & Aliases | Description | @@ -68,3 +41,32 @@ $:n("[0 1 3 1 5 4]/4") .attack(rotB.range(0,0.1)) .sound("sawtooth").cpm(tempo) ``` + +## Setup SSL for Local Development + +`DeviceMotionEvent` only works with HTTPS, so you'll need to enable SSL for local development. +Try installing an SSL plugin for Vite. + +```sh +cd website +pnpm install -D @vitejs/plugin-basic-ssl +``` + +add the basicSsl plugin to the defineConfig block in `strudel/website/astro.config.mjs` + +```js +vite: { + plugins: [basicSsl()], + server: { + host: '0.0.0.0', // Ensures it binds to all network interfaces + // https: { + // key: '../../key.pem', // + // cert: '../../cert.pem', + // }, + }, +}, +``` + +generate an SSL certificate to avoid security warnings. + +`openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout key.pem -out cert.pem` From 1f5f1a63a6d4e1bd7b2c2bc8c2ab6e9bcdc011ff Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 31 Jan 2025 08:23:52 +0100 Subject: [PATCH 6/9] docs: cpm -> setcpm --- packages/motion/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/motion/README.md b/packages/motion/README.md index f122a675e..4fc343f93 100644 --- a/packages/motion/README.md +++ b/packages/motion/README.md @@ -20,14 +20,14 @@ npm i @strudel/motion --save ## Example -``` +```js enableMotion() //enable DeviceMotion -let tempo = 200 +setcpm(200/4) $_: accX.segment(16).gain().log() -$:n("[0 1 3 1 5 4]/4") +$:n("0 1 3 1 5 4") .scale("Bb:lydian") .sometimesBy(0.5,sub(note(12))) .lpf(gravityY.range(20,1000)) @@ -39,7 +39,7 @@ $:n("[0 1 3 1 5 4]/4") .delay(rotG.range(0,1)) .decay(rotA.range(0,1)) .attack(rotB.range(0,0.1)) - .sound("sawtooth").cpm(tempo) + .sound("sawtooth") ``` ## Setup SSL for Local Development From bcb1da826ef4ae52a06b682782f9368bacc74c1f Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 31 Jan 2025 08:26:09 +0100 Subject: [PATCH 7/9] fix: doc example --- packages/motion/docs/devicemotion.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/motion/docs/devicemotion.mdx b/packages/motion/docs/devicemotion.mdx index ebf1dbc4a..9afccc890 100644 --- a/packages/motion/docs/devicemotion.mdx +++ b/packages/motion/docs/devicemotion.mdx @@ -54,8 +54,8 @@ Here's a simple example that uses device motion to control a synthesizer: client:idle tune={`enableMotion() // Create a simple melody -$:n("0 1 3 5"). -.scale("C major") +$:n("0 1 3 5") +.scale("C:major") // Use tilt (gravity) to control filter .lpf(gravityY.range(200, 2000)) // tilt forward/back for filter cutoff // Use rotation to control effects From 5cef1a8cf943d55757b4e4c701d1a69c10b00057 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 31 Jan 2025 08:40:00 +0100 Subject: [PATCH 8/9] refactor: simplify test runtime --- test/runtime.mjs | 92 +++++++++++++----------------------------------- 1 file changed, 25 insertions(+), 67 deletions(-) diff --git a/test/runtime.mjs b/test/runtime.mjs index 18d4a29e8..b0a329a47 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -74,72 +74,31 @@ const toneHelpersMocked = { highpass: mockNode, }; -strudel.Pattern.prototype.osc = function () { - return this; -}; -strudel.Pattern.prototype.csound = function () { - return this; -}; -strudel.Pattern.prototype.tone = function () { - return this; -}; -strudel.Pattern.prototype.webdirt = function () { - return this; -}; - -// draw mock -strudel.Pattern.prototype.pianoroll = function () { - return this; -}; - -// speak mock -strudel.Pattern.prototype.speak = function () { - return this; -}; - -// webaudio mock -strudel.Pattern.prototype.wave = function () { - return this; -}; -strudel.Pattern.prototype.filter = function () { - return this; -}; -strudel.Pattern.prototype.adsr = function () { - return this; -}; -strudel.Pattern.prototype.webaudio = function () { - return this; -}; -strudel.Pattern.prototype.soundfont = function () { - return this; -}; -// tune mock -strudel.Pattern.prototype.tune = function () { - return this; -}; - -strudel.Pattern.prototype.midi = function () { - return this; -}; - -strudel.Pattern.prototype._scope = function () { - return this; -}; -strudel.Pattern.prototype._spiral = function () { - return this; -}; -strudel.Pattern.prototype._pitchwheel = function () { - return this; -}; -strudel.Pattern.prototype._pianoroll = function () { - return this; -}; -strudel.Pattern.prototype._spectrum = function () { - return this; -}; -strudel.Pattern.prototype.markcss = function () { - return this; -}; +[ + 'osc', + 'csound', + 'tone', + 'webdirt', + 'pianoroll', + 'speak', + 'wave', + 'filter', + 'adsr', + 'webaudio', + 'soundfont', + 'tune', + 'midi', + '_scope', + '_spiral', + '_pitchwheel', + '_pianoroll', + '_spectrum', + 'markcss', +].forEach((mock) => { + strudel.Pattern.prototype[mock] = function () { + return this; + }; +}); const uiHelpersMocked = { backgroundImage: id, @@ -193,7 +152,6 @@ evalScope( loadcsound, setcps: id, Clock: {}, // whatever - // Tone, }, ); From 75f5f456527739982f66a00fe36291ea0f54db46 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 31 Jan 2025 08:40:16 +0100 Subject: [PATCH 9/9] fix: bring back examples test + ignore device motion examples --- test/examples.test.mjs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/examples.test.mjs diff --git a/test/examples.test.mjs b/test/examples.test.mjs new file mode 100644 index 000000000..997c1cf37 --- /dev/null +++ b/test/examples.test.mjs @@ -0,0 +1,36 @@ +import { queryCode } from './runtime.mjs'; +import { describe, it } from 'vitest'; +import doc from '../doc.json'; + +const skippedExamples = [ + 'absoluteOrientationGamma', + 'absoluteOrientationBeta', + 'absoluteOrientationAlpha', + 'orientationGamma', + 'orientationBeta', + 'orientationAlpha', + 'rotationGamma', + 'rotationBeta', + 'rotationAlpha', + 'gravityZ', + 'gravityY', + 'gravityX', + 'accelerationZ', + 'accelerationY', + 'accelerationX', +]; + +describe('runs examples', () => { + const { docs } = doc; + docs.forEach(async (doc) => { + if (skippedExamples.includes(doc.name)) { + return; + } + doc.examples?.forEach((example, i) => { + it(`example "${doc.name}" example index ${i}`, async ({ expect }) => { + const haps = await queryCode(example, 4); + expect(haps).toMatchSnapshot(); + }); + }); + }); +});