diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs
index 34a63c93..72d3b100 100644
--- a/packages/core/pattern.mjs
+++ b/packages/core/pattern.mjs
@@ -3119,9 +3119,9 @@ Pattern.prototype.xfade = function (pos, b) {
* especially useful for creating rhythms
* @name beat
* @example
- * s("bd").beat("0:7:10", 16)
+ * s("bd").beat("0,7,10", 16)
* @example
- * s("sd").beat("4:12", 16)
+ * s("sd").beat("4,12", 16)
*/
const __beat = (join) => (t, div, pat) => {
t = Fraction(t).mod(div);
diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs
index 2c6742cd..d874c968 100644
--- a/packages/draw/pianoroll.mjs
+++ b/packages/draw/pianoroll.mjs
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import { Pattern, noteToMidi, freqToMidi } from '@strudel/core';
+import { Pattern, noteToMidi, freqToMidi, isPattern } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
@@ -36,35 +36,9 @@ const getValue = (e) => {
return value;
};
-Pattern.prototype.pianoroll = function (options = {}) {
- let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options;
-
- let from = -cycles * playhead;
- let to = cycles * (1 - playhead);
- const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
- this.draw(
- (haps, time) => {
- pianoroll({
- ...options,
- time,
- ctx,
- haps: haps.filter((hap) => inFrame(hap, time)),
- });
- },
- {
- lookbehind: from - overscan,
- lookahead: to + overscan,
- id,
- },
- );
- return this;
-};
-
-// this function allows drawing a pianoroll without ties to Pattern.prototype
-// it will probably replace the above in the future
-
/**
- * Displays a midi-style piano roll
+ * Visualises a pattern as a scrolling 'pianoroll', displayed in the background of the editor. To show a pianoroll for all running patterns, use `all(pianoroll)`. To have a pianoroll appear below
+ * a pattern instead, prefix with `_`, e.g.: `sound("bd sd")._pianoroll()`.
*
* @name pianoroll
* @synonyms punchcard
@@ -93,15 +67,51 @@ Pattern.prototype.pianoroll = function (options = {}) {
* @param {integer} minMidi minimum note value to display on the value axis - defaults to 10
* @param {integer} maxMidi maximum note value to display on the value axis - defaults to 90
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
- *
+ * @see _pianoroll
* @example
* note("c2 a2 eb2")
* .euclid(5,8)
* .s('sawtooth')
* .lpenv(4).lpf(300)
- * ._pianoroll({ labels: 1 })
+ * .pianoroll({ labels: 1 })
*/
-export function pianoroll({
+
+Pattern.prototype.pianoroll = function (options = {}) {
+ let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options;
+
+ let from = -cycles * playhead;
+ let to = cycles * (1 - playhead);
+ const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
+ this.draw(
+ (haps, time) => {
+ __pianoroll({
+ ...options,
+ time,
+ ctx,
+ haps: haps.filter((hap) => inFrame(hap, time)),
+ });
+ },
+ {
+ lookbehind: from - overscan,
+ lookahead: to + overscan,
+ id,
+ },
+ );
+ return this;
+};
+
+export function pianoroll(arg) {
+ if (isPattern(arg)) {
+ // Single argument as a pattern
+ // (to support `all(pianoroll)`)
+ return arg.pianoroll();
+ }
+ // Single argument with option - return function to get the pattern
+ // (to support `all(pianoroll(options))`)
+ return (pat) => pat.pianoroll(arg);
+}
+
+export function __pianoroll({
time,
haps,
cycles = 4,
@@ -278,7 +288,7 @@ export function getDrawOptions(drawTime, options = {}) {
export const getPunchcardPainter =
(options = {}) =>
(ctx, time, haps, drawTime) =>
- pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
+ __pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
Pattern.prototype.punchcard = function (options) {
return this.onPaint(getPunchcardPainter(options));
@@ -302,5 +312,5 @@ Pattern.prototype.wordfall = function (options) {
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
- pianoroll({ ...getDrawOptions(drawTime), ...rest });
+ __pianoroll({ ...getDrawOptions(drawTime), ...rest });
}
diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs
index 75f7904e..0d02a37c 100644
--- a/packages/mqtt/mqtt.mjs
+++ b/packages/mqtt/mqtt.mjs
@@ -23,6 +23,9 @@ function onMessageArrived(message) {
function onFailure(err) {
console.error('Connection failed: ', err);
+ if (typeof window !== 'undefined') {
+ document.cookie = 'mqtt_pass=';
+ }
}
Pattern.prototype.mqtt = function (
@@ -35,12 +38,17 @@ Pattern.prototype.mqtt = function (
) {
const key = host + '-' + client;
let connected = false;
+ let password_entered = false;
+
if (!client) {
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
}
function onConnect() {
console.log('Connected to mqtt broker');
connected = true;
+ if (password_entered) {
+ document.cookie = 'mqtt_pass=' + password;
+ }
}
let cx;
@@ -58,6 +66,17 @@ Pattern.prototype.mqtt = function (
if (username) {
props.userName = username;
+ if (typeof password === 'undefined' && typeof window !== 'undefined') {
+ const cookie = /mqtt_pass=(\w+)/.exec(window.document.cookie);
+ if (cookie) {
+ password = cookie[1];
+ }
+ if (typeof password === 'undefined') {
+ password = prompt('Please enter MQTT server password');
+ password_entered = true;
+ }
+ }
+
props.password = password;
}
cx.connect(props);
diff --git a/packages/reference/README.md b/packages/reference/README.md
new file mode 100644
index 00000000..8ff16259
--- /dev/null
+++ b/packages/reference/README.md
@@ -0,0 +1,8 @@
+# @strudel/reference
+
+this package contains metadata for all documented strudel functions, useful to implement a reference.
+
+```js
+import { reference } from '@strudel/reference';
+console.log(reference)
+```
diff --git a/packages/reference/index.mjs b/packages/reference/index.mjs
new file mode 100644
index 00000000..deeb6039
--- /dev/null
+++ b/packages/reference/index.mjs
@@ -0,0 +1,2 @@
+import jsdoc from '../../doc.json';
+export const reference = jsdoc;
diff --git a/packages/reference/package.json b/packages/reference/package.json
new file mode 100644
index 00000000..1782fca9
--- /dev/null
+++ b/packages/reference/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@strudel/reference",
+ "version": "1.1.0",
+ "description": "Headless reference of all strudel functions",
+ "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": [
+ "tidalcycles",
+ "strudel",
+ "pattern",
+ "livecoding",
+ "algorave"
+ ],
+ "author": "Felix Roos ",
+ "contributors": [
+ "Alex McLean "
+ ],
+ "license": "AGPL-3.0-or-later",
+ "bugs": {
+ "url": "https://github.com/tidalcycles/strudel/issues"
+ },
+ "homepage": "https://github.com/tidalcycles/strudel#readme",
+ "dependencies": {
+ },
+ "devDependencies": {
+ "vite": "^5.0.10"
+ }
+}
diff --git a/packages/reference/vite.config.js b/packages/reference/vite.config.js
new file mode 100644
index 00000000..5df3edc1
--- /dev/null
+++ b/packages/reference/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/pnpm-lock.yaml b/pnpm-lock.yaml
index 6cb259a9..24529652 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -360,6 +360,12 @@ importers:
specifier: ^5.0.10
version: 5.4.9(@types/node@22.7.6)(terser@5.36.0)
+ packages/reference:
+ devDependencies:
+ vite:
+ specifier: ^5.0.10
+ version: 5.4.9(@types/node@22.7.6)(terser@5.36.0)
+
packages/repl:
dependencies:
'@strudel/codemirror':
@@ -7759,7 +7765,6 @@ packages:
workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
- deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
workbox-navigation-preload@7.0.0:
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap
index c071b536..c8d53858 100644
--- a/test/__snapshots__/examples.test.mjs.snap
+++ b/test/__snapshots__/examples.test.mjs.snap
@@ -957,18 +957,30 @@ exports[`runs examples > example "bank" example index 0 1`] = `
exports[`runs examples > example "beat" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd ]",
+ "[ 7/16 → 1/2 | s:bd ]",
+ "[ 5/8 → 11/16 | s:bd ]",
"[ 1/1 → 17/16 | s:bd ]",
+ "[ 23/16 → 3/2 | s:bd ]",
+ "[ 13/8 → 27/16 | s:bd ]",
"[ 2/1 → 33/16 | s:bd ]",
+ "[ 39/16 → 5/2 | s:bd ]",
+ "[ 21/8 → 43/16 | s:bd ]",
"[ 3/1 → 49/16 | s:bd ]",
+ "[ 55/16 → 7/2 | s:bd ]",
+ "[ 29/8 → 59/16 | s:bd ]",
]
`;
exports[`runs examples > example "beat" example index 1 1`] = `
[
- "[ 1/48 → 1/12 | s:sd ]",
- "[ 49/48 → 13/12 | s:sd ]",
- "[ 97/48 → 25/12 | s:sd ]",
- "[ 145/48 → 37/12 | s:sd ]",
+ "[ 1/4 → 5/16 | s:sd ]",
+ "[ 3/4 → 13/16 | s:sd ]",
+ "[ 5/4 → 21/16 | s:sd ]",
+ "[ 7/4 → 29/16 | s:sd ]",
+ "[ 9/4 → 37/16 | s:sd ]",
+ "[ 11/4 → 45/16 | s:sd ]",
+ "[ 13/4 → 53/16 | s:sd ]",
+ "[ 15/4 → 61/16 | s:sd ]",
]
`;
diff --git a/website/src/config.ts b/website/src/config.ts
index dd003c18..a404f542 100644
--- a/website/src/config.ts
+++ b/website/src/config.ts
@@ -100,6 +100,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'Coding syntax', link: 'learn/code' },
{ text: 'Pitch', link: 'understand/pitch' },
{ text: 'Cycles', link: 'understand/cycles' },
+ { text: 'Voicings', link: 'understand/voicings' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
],
diff --git a/website/src/pages/understand/voicings.mdx b/website/src/pages/understand/voicings.mdx
new file mode 100644
index 00000000..7d9c13d9
--- /dev/null
+++ b/website/src/pages/understand/voicings.mdx
@@ -0,0 +1,325 @@
+---
+title: Understanding Chord Voicings
+layout: ../../layouts/MainLayout.astro
+---
+
+import { MiniRepl } from '../../docs/MiniRepl';
+import { PitchSlider } from '../../components/PitchSlider';
+import Box from '@components/Box.astro';
+
+# Understanding Chords and Voicings
+
+Let's dig deeper into how chords and voicings work in strudel.
+I'll try to keep theory jargon to a minimum, so hopefully this is approachable for anyone interested.
+
+## What is a chord
+
+Playing more than one note at a time is generally called a `chord`. Here's an example:
+
+").room(.5)`} />
+
+Here's the same with midi numbers:
+
+").room(.5)`} />
+
+Here, we have two 3-note chords played in a loop.
+You could already stop here and write chords in this style, which is totally fine and gives you control over individual notes.
+One downside is that it can be difficult to find good sounding chords and maybe you're yearning for a way to organize chords in some other way.
+
+## Labeling Chords
+
+Chords are typically given different labels depending on the relationship of the notes within.
+In the number example above, we have `48,51,55` and `53,57,60`.
+
+To analyze the relationship of those notes, they are typically compared to some `root`, which is often the lowest note.
+In our case, the `roots` would be `48` (= `c3`) and `53` (= `f3`).
+We can express the same chords relative to those `roots` like this:
+
+".add("<48 53>")).room(.5)`} />
+
+Now within each chord, each number represents the distance from the root.
+A distance between pitches is typically called `interval`, but let's stick to distance for now.
+
+Now we can see that our 2 chords are actually quite similar, as the only difference is the middle note (and the root of course).
+They are part of a group of chords called `triads` which are chords with 3 notes.
+
+### Triads
+
+These 4 shapes are the most common types of `triads` you will encounter:
+
+| shape | label |
+| ----- | ---------- |
+| 0,4,7 | major |
+| 0,3,7 | minor |
+| 0,3,6 | diminished |
+| 0,4,8 | augmented |
+
+Here they are in succession:
+
+".add("60"))
+.room(.5)._pitchwheel()`}
+/>
+
+Many types of music often only use minor and major chords, so we already have the knowledge to accompany songs. Here's one:
+
+\`.add(\`<
+a c d f
+a e a e
+>\`)).room(.5)`}
+/>
+
+These are the chords for "The House of the Rising Sun" by The Animals.
+So far, it doesn't sound too exciting, but at least it's recognizable.
+
+## Voicings
+
+A `voicing` is one of many ways a certain chord shape can be arranged.
+The term comes from choral music, where chords can be sung in different ways by assigning different notes to each voice.
+For example we could add 12 semitones to one or more notes in the chord:
+
+".add("48"))
+.room(.5)`}
+/>
+
+Notes that are 12 semitone steps apart (= 1 `octave`) are considered to be equal in a harmonic sense, which is why they get the same note letter.
+Here's the same example with note letters:
+
+")
+.room(.5)`}
+/>
+
+These types of voicings are also called `inversions`. There are many other ways we could `voice` this minor chord:
+
+".add("48"))
+.room(.5)`}
+/>
+
+Here we are changing the flavour of the chord slightly by
+
+1. doubling notes 12 steps higher,
+2. using very wide distances
+3. omitting notes
+
+## Voice Leading
+
+When we want to meaningfully connect chords in a sequence, the chosen voicings affect the way each chord transitions to the next.
+Let's revisit "The House of the Rising Sun", this time using our newly acquired voicing techniques:
+
+\`.add(\`<
+a c d f
+a e a e
+>\`)).room(.5)`}
+ punchcard
+/>
+
+These voicings make the chords sound more connected and less jumpy, compared to the earlier version, which didn't focus on voicing.
+The way chords interact is also called `voice leading`, reminiscent of how an
+individual choir voice would move through a sequence of chords.
+
+For example, try singing the top voice in the above example. Then try the same
+on the example not focusing on voice leading. Which one's easier?
+
+Naturally, there are many ways a progression of chords could be voiced and there is no definitive right or wrong.
+
+## Chord Symbols
+
+Musicians playing chord-based music often use a `lead sheet`, which is a simplified notation for a piece of music.
+These sheets condense the essential elements, such as chords, into symbols that make the music easy to read and follow.
+For example, a lead sheet for "The House of the Rising Sun" might include chords written like this:
+
+```
+Am | C | D | F
+Am | E | Am | E
+```
+
+Here, each symbol consists of the `root` of the chord and optionally an `m` to signal it's a minor chord (just the root note means it's major).
+We could mirror that notation in strudel using the `pick` function:
+
+"
+ .pick({
+ Am: "57,60,64",
+ C: "55,60,64",
+ D: "50,57,66",
+ F: "57,60,65",
+ E: "56,59,64",
+ })
+ .note().room(.5)`}
+ punchcard
+/>
+
+## The voicing function
+
+Coming up with good sounding voicings that connect well can be a difficult and time consuming process.
+The `chord` and `voicing` functions can be used to automate that:
+
+").voicing().room(.5)`} punchcard />
+
+Here we're also using chord symbols but the voicings will be automatically generated with smooth `voice leading`, minimizing jumps.
+It is inspired by the way a piano or guitar player would pick chords to accompany a song.
+
+## Voicing Dictionaries
+
+The voicing function internally uses so called `voicing dictionaries`, which can also be customized:
+
+")
+ .dict('house').anchor(66)
+ .voicing().room(.5)`}
+ punchcard
+/>
+
+In a `voicing dictionary`, each chord symbol is assigned one or more voicings.
+The `voicing` function then picks the voicing that is closest to the `anchor` (defaults to `c5`).
+
+The handy thing about this approach is that a `voicing dictionary` can be used to play any chord progression with automated voice leading!
+
+## The default dictionary
+
+When using the default dictionary, you can use these chord symbols:
+
+```
+2 5 6 7 9 11 13 69 add9
+o h sus ^ - ^7 -7 7sus
+h7 o7 ^9 ^13 ^7#11 ^9#11
+^7#5 -6 -69 -^7 -^9 -9
+-add9 -11 -7b5 h9 -b6 -#5
+7b9 7#9 7#11 7b5 7#5 9#11
+9b5 9#5 7b13 7#9#5 7#9b5
+7#9#11 7b9#11 7b9b5 7b9#5
+7b9#9 7b9b13 7alt 13#11
+13b9 13#9 7b9sus 7susadd3
+9sus 13sus 7b13sus
+aug M m M7 m7 M9 M13
+M7#11 M9#11 M7#5 m6 m69
+m^7 -M7 m^9 -M9 m9 madd9
+m11 m7b5 mb6 m#5 mM7 mM9
+```
+
+The available chords and the format is very much inspired by [ireal pro chords](https://technimo.helpshift.com/hc/en/3-ireal-pro/faq/88-chord-symbols-used-in-ireal-pro/).
+Some symbols are synonymous:
+
+- "-" is the same as "m", for example C-7 = Cm7
+- "^" is the same as "M", for example C^7 = CM7
+- "+" is the same as "aug"
+
+You can decide which ones you prefer. There is no international standard for these symbols.
+To get a full chord, the symbols have to be prefixed with a root pitch, e.g. D7#11 is the 7#11 chord relative to the pitch D.
+
+Here are all possible chords with root C:
+
+\`).voicing().room(.5)`}
+ punchcard
+/>
+
+Note that the default dictionary contains multiple ways (= `voicings`) to play each chord symbol.
+By default, the `voicing` function tries to minimize jumps.
+You can alter the picked voicings in various ways, which are now explained in further detail:
+
+## anchor
+
+The `anchor` is a note that is used to align the voicings to:
+
+").chord("C").voicing().room(.5)`} punchcard />
+
+By default, the anchor is the highest possible note the voicing can contain.
+When deciding which voicing of the dictionary to pick for a certain chord, the voicing with a top note closest to the anchor wins.
+
+Note that the anchors in the above example match up with the top notes in the pianoroll.
+Like `note`, anchor accepts either midi numbers or note names.
+
+## mode
+
+With `mode`, you can change the way the voicing relates to the `anchor`:
+
+").chord("C").anchor("c5").voicing().room(.5)`}
+ punchcard
+/>
+
+The modes are:
+
+- `below`: the top note of the voicing is lower than or equal to the anchor (default)
+- `above`: the bottom note of the voicing is higher than or equal to the anchor
+- `duck`: the top note of the voicing is lower than the anchor
+- `root`: the bottom note of the voicing is always the root note closest to the anchor
+
+The `anchor` can also be set from within the `mode` function:
+
+:c5").chord("C").voicing().room(.5)`} punchcard />
+
+## n
+
+The `n` control can be used with `voicing` to select individual notes:
+
+>").voicing()
+.clip("4 3 2 1").room(.5)`}
+ punchcard
+/>
+
+## Example
+
+Here's an example of a Jazz Blues in F:
+
+\`)
+$: n("7 8 [10 9] 8").set(chords).voicing().dec(.2)
+$: chords.struct("- x - x").voicing().room(.5)
+$: n("0 - 1 -").set(chords).mode("root:g2").voicing()
+`}
+ punchcard
+/>
+
+The chords are reused for melody, chords and bassline of the tune.