Skip to content

Commit

Permalink
Merge branch 'tidalcycles:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
yaxu authored Dec 22, 2024
2 parents 5136a06 + 0531369 commit 8f3cc76
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 14 deletions.
4 changes: 4 additions & 0 deletions packages/core/cyclist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/mqtt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @strudel/serial

This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
87 changes: 87 additions & 0 deletions packages/mqtt/mqtt.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
mqtt.mjs - for patterning the internet of things from strudel
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.mjs>
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 <https://www.gnu.org/licenses/>.
*/

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 });
});
};
38 changes: 38 additions & 0 deletions packages/mqtt/package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"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"
}
}
19 changes: 19 additions & 0 deletions packages/mqtt/vite.config.js
Original file line number Diff line number Diff line change
@@ -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',
},
});
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/mqtt": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*",
Expand Down
95 changes: 81 additions & 14 deletions website/src/pages/learn/input-output.mdx
Original file line number Diff line number Diff line change
@@ -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?)

Expand Down Expand Up @@ -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 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.

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.
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!

Expand Down Expand Up @@ -86,3 +89,67 @@ Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
<br />

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:

<MiniRepl
client:only="react"
tune={`"hello world"
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
)`}

/>

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:

<MiniRepl
client:only="react"
tune={`sound("sax(3,8)").speed("2 3")
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
)`}
/>

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.
1 change: 1 addition & 0 deletions website/src/repl/util.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function loadModules() {
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
import('@strudel/mqtt'),
];
if (isTauri()) {
modules = modules.concat([
Expand Down

0 comments on commit 8f3cc76

Please sign in to comment.