diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 472e83a8..bd2db122 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -68,6 +68,10 @@ export class Cyclist { // see https://github.com/tidalcycles/strudel/pull/1004 const deadline = targetTime - phase; onTrigger?.(hap, deadline, duration, this.cps, targetTime); + if (hap.value.cps !== undefined && this.cps != hap.value.cps) { + this.cps = hap.value.cps; + this.num_ticks_since_cps_change = 0; + } } }); } catch (e) { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 016eca3e..560c9d33 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -152,9 +152,12 @@ export function repl({ shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - pattern = stack(...Object.values(pPatterns)); - } - if (allTransform) { + let patterns = Object.values(pPatterns); + if (allTransform) { + patterns = patterns.map(allTransform); + } + pattern = stack(...patterns); + } else if (allTransform) { pattern = allTransform(pattern); } if (!isPattern(pattern)) { diff --git a/packages/mqtt/README.md b/packages/mqtt/README.md new file mode 100644 index 00000000..7952b553 --- /dev/null +++ b/packages/mqtt/README.md @@ -0,0 +1,3 @@ +# @strudel/serial + +This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers. diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs new file mode 100644 index 00000000..75f7904e --- /dev/null +++ b/packages/mqtt/mqtt.mjs @@ -0,0 +1,87 @@ +/* +mqtt.mjs - for patterning the internet of things from strudel +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { Pattern, isPattern } from '@strudel/core'; +import Paho from 'paho-mqtt'; + +const connections = {}; + +// Handle connection loss +function onConnectionLost(responseObject) { + if (responseObject.errorCode !== 0) { + console.error(' mqtt connection lost: ', responseObject.errorMessage); + } +} + +// Handle received messages +function onMessageArrived(message) { + console.log('incoming mqtt message: ', message.payloadString); // prettier-ignore +} + +function onFailure(err) { + console.error('Connection failed: ', err); +} + +Pattern.prototype.mqtt = function ( + username = undefined, + password = undefined, + topic = undefined, + host = 'wss://localhost:8883/', + client = undefined, + latency = 0, +) { + const key = host + '-' + client; + let connected = false; + if (!client) { + client = 'strudel-' + String(Math.floor(Math.random() * 1000000)); + } + function onConnect() { + console.log('Connected to mqtt broker'); + connected = true; + } + + let cx; + if (connections[key]) { + cx = connections[key]; + } else { + cx = new Paho.Client(host, client); + cx.onConnectionLost = onConnectionLost; + cx.onMessageArrived = onMessageArrived; + const props = { + onSuccess: onConnect, + onFailure: onFailure, + useSSL: true, + }; + + if (username) { + props.userName = username; + props.password = password; + } + cx.connect(props); + } + return this.withHap((hap) => { + const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + if (!connected) { + return; + } + let message = ''; + if (typeof hap.value === 'object') { + message = JSON.stringify(hap.value); + } else { + message = hap.value; + } + message = new Paho.Message(message); + message.destinationName = topic; + + const offset = (targetTime - currentTime + latency) * 1000; + + window.setTimeout(function () { + cx.send(message); + }, offset); + }; + return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); + }); +}; diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json new file mode 100644 index 00000000..b6be50f5 --- /dev/null +++ b/packages/mqtt/package.json @@ -0,0 +1,38 @@ +{ + "name": "@strudel/mqtt", + "version": "1.1.0", + "description": "MQTT API for strudel", + "main": "mqtt.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": "Alex McLean ", + "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:*", + "paho-mqtt": "^1.1.0" + }, + "devDependencies": { + "vite": "^5.0.10" + } +} diff --git a/packages/mqtt/vite.config.js b/packages/mqtt/vite.config.js new file mode 100644 index 00000000..b45b7ca6 --- /dev/null +++ b/packages/mqtt/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, 'mqtt.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 2e22c1db..38918c0a 100644 --- a/website/package.json +++ b/website/package.json @@ -34,6 +34,7 @@ "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", "@strudel/motion": "workspace:*", + "@strudel/mqtt": "workspace:*", "@strudel/osc": "workspace:*", "@strudel/serial": "workspace:*", "@strudel/soundfonts": "workspace:*", diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 93cbfdc5..14e46ba7 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -1,19 +1,20 @@ --- -title: MIDI & OSC +title: MIDI, OSC & MQTT layout: ../../layouts/MainLayout.astro --- import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# MIDI and OSC +# MIDI, OSC and MQTT -The default audio output of Strudel uses the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). -It is also possible to use Strudel with MIDI and OSC / [SuperDirt](https://github.com/musikinformatik/SuperDirt/) instead. +Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://github.com/tidalcycles/strudel/tree/main/packages/superdough). -# MIDI API +It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the [SuperDirt](https://github.com/musikinformatik/SuperDirt/) synthesiser commonly used with Strudel's sibling [TidalCycles](https://tidalcycles.org/)), or the MQTT 'internet of things' protocol. -Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi). +# MIDI + +Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern: ## midi(outputName?) @@ -45,20 +46,22 @@ But you can also control cc messages separately like this: $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`} /> -# OSC/SuperDirt API +# OSC/SuperDirt/StrudelDirt -In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. -Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run. +In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software. + +There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt) ## Prequisites -Getting [SuperDirt](https://github.com/musikinformatik/SuperDirt/) to work with Strudel, you need to +To get SuperDirt to work with Strudel, you need to 1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info. -2. install [node.js](https://nodejs.org/en/) -3. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed) -4. run `pnpm i` in the strudel directory -5. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider +2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel +3. install [node.js](https://nodejs.org/en/) +4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed) +5. run `pnpm i` in the strudel directory +6. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider Now you're all set! @@ -86,3 +89,67 @@ Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
But can we use Strudel [offline](/learn/pwa)? + +# MQTT + +MQTT is a lightweight network protocol, designed for 'internet of things' devices. For use with strudel, you will +need access to an MQTT server known as a 'broker' configured to accept secure 'websocket' connections. You could +run one yourself (e.g. by running [mosquitto](https://mosquitto.org/)), although getting an SSL certificate that +your web browser will trust might be a bit tricky for those without systems administration experience. +Alternatively, you can use [a public broker](https://www.hivemq.com/mqtt/public-mqtt-broker/). + +Strudel does not yet support receiving messages over MQTT, only sending them. + +## Usage + +The following example shows how to send a pattern to an MQTT broker: + + + +Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools: + +``` +> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern" +hello +world +hello +world +... +``` + +Control patterns will be encoded as JSON, for example: + + + +Will send messages like the following: + +``` +{"s":"sax","speed":2} +{"s":"sax","speed":2} +{"s":"sax","speed":3} +{"s":"sax","speed":2} +... +``` + +Libraries for receiving MQTT are available for many programming languages. diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 7eede36e..c5bec661 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -82,6 +82,7 @@ export function loadModules() { import('@strudel/csound'), import('@strudel/tidal'), import('@strudel/motion'), + import('@strudel/mqtt'), ]; if (isTauri()) { modules = modules.concat([