Merge remote-tracking branch 'origin/main' into lu/add-drum-suffixes
This commit is contained in:
commit
4f6e38f714
11 changed files with 481 additions and 41 deletions
|
|
@ -3119,9 +3119,9 @@ Pattern.prototype.xfade = function (pos, b) {
|
||||||
* especially useful for creating rhythms
|
* especially useful for creating rhythms
|
||||||
* @name beat
|
* @name beat
|
||||||
* @example
|
* @example
|
||||||
* s("bd").beat("0:7:10", 16)
|
* s("bd").beat("0,7,10", 16)
|
||||||
* @example
|
* @example
|
||||||
* s("sd").beat("4:12", 16)
|
* s("sd").beat("4,12", 16)
|
||||||
*/
|
*/
|
||||||
const __beat = (join) => (t, div, pat) => {
|
const __beat = (join) => (t, div, pat) => {
|
||||||
t = Fraction(t).mod(div);
|
t = Fraction(t).mod(div);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||||
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/>.
|
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, noteToMidi, freqToMidi } from '@strudel/core';
|
import { Pattern, noteToMidi, freqToMidi, isPattern } from '@strudel/core';
|
||||||
import { getTheme, getDrawContext } from './draw.mjs';
|
import { getTheme, getDrawContext } from './draw.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
|
|
@ -36,35 +36,9 @@ const getValue = (e) => {
|
||||||
return value;
|
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
|
* @name pianoroll
|
||||||
* @synonyms punchcard
|
* @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} 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 {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
|
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
|
||||||
*
|
* @see _pianoroll
|
||||||
* @example
|
* @example
|
||||||
* note("c2 a2 eb2")
|
* note("c2 a2 eb2")
|
||||||
* .euclid(5,8)
|
* .euclid(5,8)
|
||||||
* .s('sawtooth')
|
* .s('sawtooth')
|
||||||
* .lpenv(4).lpf(300)
|
* .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,
|
time,
|
||||||
haps,
|
haps,
|
||||||
cycles = 4,
|
cycles = 4,
|
||||||
|
|
@ -278,7 +288,7 @@ export function getDrawOptions(drawTime, options = {}) {
|
||||||
export const getPunchcardPainter =
|
export const getPunchcardPainter =
|
||||||
(options = {}) =>
|
(options = {}) =>
|
||||||
(ctx, time, haps, drawTime) =>
|
(ctx, time, haps, drawTime) =>
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
|
__pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
|
||||||
|
|
||||||
Pattern.prototype.punchcard = function (options) {
|
Pattern.prototype.punchcard = function (options) {
|
||||||
return this.onPaint(getPunchcardPainter(options));
|
return this.onPaint(getPunchcardPainter(options));
|
||||||
|
|
@ -302,5 +312,5 @@ Pattern.prototype.wordfall = function (options) {
|
||||||
|
|
||||||
export function drawPianoroll(options) {
|
export function drawPianoroll(options) {
|
||||||
const { drawTime, ...rest } = options;
|
const { drawTime, ...rest } = options;
|
||||||
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
__pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ function onMessageArrived(message) {
|
||||||
|
|
||||||
function onFailure(err) {
|
function onFailure(err) {
|
||||||
console.error('Connection failed: ', err);
|
console.error('Connection failed: ', err);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
document.cookie = 'mqtt_pass=';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.mqtt = function (
|
Pattern.prototype.mqtt = function (
|
||||||
|
|
@ -35,12 +38,17 @@ Pattern.prototype.mqtt = function (
|
||||||
) {
|
) {
|
||||||
const key = host + '-' + client;
|
const key = host + '-' + client;
|
||||||
let connected = false;
|
let connected = false;
|
||||||
|
let password_entered = false;
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
|
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
|
||||||
}
|
}
|
||||||
function onConnect() {
|
function onConnect() {
|
||||||
console.log('Connected to mqtt broker');
|
console.log('Connected to mqtt broker');
|
||||||
connected = true;
|
connected = true;
|
||||||
|
if (password_entered) {
|
||||||
|
document.cookie = 'mqtt_pass=' + password;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cx;
|
let cx;
|
||||||
|
|
@ -58,6 +66,17 @@ Pattern.prototype.mqtt = function (
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
props.userName = 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;
|
props.password = password;
|
||||||
}
|
}
|
||||||
cx.connect(props);
|
cx.connect(props);
|
||||||
|
|
|
||||||
8
packages/reference/README.md
Normal file
8
packages/reference/README.md
Normal file
|
|
@ -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)
|
||||||
|
```
|
||||||
2
packages/reference/index.mjs
Normal file
2
packages/reference/index.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
import jsdoc from '../../doc.json';
|
||||||
|
export const reference = jsdoc;
|
||||||
39
packages/reference/package.json
Normal file
39
packages/reference/package.json
Normal file
|
|
@ -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 <flix91@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Alex McLean <alex@slab.org>"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/reference/vite.config.js
Normal file
19
packages/reference/vite.config.js
Normal file
|
|
@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
|
|
@ -360,6 +360,12 @@ importers:
|
||||||
specifier: ^5.0.10
|
specifier: ^5.0.10
|
||||||
version: 5.4.9(@types/node@22.7.6)(terser@5.36.0)
|
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:
|
packages/repl:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/codemirror':
|
'@strudel/codemirror':
|
||||||
|
|
@ -7759,7 +7765,6 @@ packages:
|
||||||
|
|
||||||
workbox-google-analytics@7.0.0:
|
workbox-google-analytics@7.0.0:
|
||||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
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:
|
workbox-navigation-preload@7.0.0:
|
||||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||||
|
|
|
||||||
|
|
@ -957,18 +957,30 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
||||||
exports[`runs examples > example "beat" example index 0 1`] = `
|
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd ]",
|
"[ 0/1 → 1/16 | s:bd ]",
|
||||||
|
"[ 7/16 → 1/2 | s:bd ]",
|
||||||
|
"[ 5/8 → 11/16 | s:bd ]",
|
||||||
"[ 1/1 → 17/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 ]",
|
"[ 2/1 → 33/16 | s:bd ]",
|
||||||
|
"[ 39/16 → 5/2 | s:bd ]",
|
||||||
|
"[ 21/8 → 43/16 | s:bd ]",
|
||||||
"[ 3/1 → 49/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`] = `
|
exports[`runs examples > example "beat" example index 1 1`] = `
|
||||||
[
|
[
|
||||||
"[ 1/48 → 1/12 | s:sd ]",
|
"[ 1/4 → 5/16 | s:sd ]",
|
||||||
"[ 49/48 → 13/12 | s:sd ]",
|
"[ 3/4 → 13/16 | s:sd ]",
|
||||||
"[ 97/48 → 25/12 | s:sd ]",
|
"[ 5/4 → 21/16 | s:sd ]",
|
||||||
"[ 145/48 → 37/12 | 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 ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ export const SIDEBAR: Sidebar = {
|
||||||
{ text: 'Coding syntax', link: 'learn/code' },
|
{ text: 'Coding syntax', link: 'learn/code' },
|
||||||
{ text: 'Pitch', link: 'understand/pitch' },
|
{ text: 'Pitch', link: 'understand/pitch' },
|
||||||
{ text: 'Cycles', link: 'understand/cycles' },
|
{ text: 'Cycles', link: 'understand/cycles' },
|
||||||
|
{ text: 'Voicings', link: 'understand/voicings' },
|
||||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||||
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
|
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
325
website/src/pages/understand/voicings.mdx
Normal file
325
website/src/pages/understand/voicings.mdx
Normal file
|
|
@ -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:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`note("<[c3,eb3,g3] [f3,a3,c4]>").room(.5)`} />
|
||||||
|
|
||||||
|
Here's the same with midi numbers:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`note("<[48,51,55] [53,57,60]>").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:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`note("<[0,3,7] [0,4,7]>".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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`note("<[0,4,7] [0,3,7] [0,3,6] [0,4,8]>".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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
note(\`<
|
||||||
|
[0,3,7] [0,4,7] [0,4,7] [0,4,7]
|
||||||
|
[0,3,7] [0,4,7] [0,3,7] [0,4,7]
|
||||||
|
>\`.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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`note("<[0,3,7] [12,3,7] [12,15,7] [12,15,19]>".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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`note("<[c3,eb3,g3] [c4,eb3,g3] [c4,eb4,g3] [c4,eb4,g4]>")
|
||||||
|
.room(.5)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
These types of voicings are also called `inversions`. There are many other ways we could `voice` this minor chord:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`note("<[0,3,7,12] [0,15,24] [0,3,12]>".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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`note(\`<
|
||||||
|
[0,3,7] [7,12,16] [0,7,16] [4,7,12]
|
||||||
|
[0,3,7] [4,7,12] [0,3,7] [4,7,12]
|
||||||
|
>\`.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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`"<Am C D F Am E Am E>"
|
||||||
|
.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:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`chord("<Am C D F Am E Am E>").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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`addVoicings('house', {
|
||||||
|
'': ['7 12 16', '0 7 16', '4 7 12'],
|
||||||
|
'm': ['0 3 7']
|
||||||
|
})
|
||||||
|
chord("<Am C D F Am E Am E>")
|
||||||
|
.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:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`chord(\`<
|
||||||
|
C2 C5 C6 C7 C9 C11 C13 C69
|
||||||
|
Cadd9 Co Ch Csus C^ C- C^7
|
||||||
|
C-7 C7sus Ch7 Co7 C^9 C^13
|
||||||
|
C^7#11 C^9#11 C^7#5 C-6 C-69
|
||||||
|
C-^7 C-^9 C-9 C-add9 C-11
|
||||||
|
C-7b5 Ch9 C-b6 C-#5 C7b9
|
||||||
|
C7#9 C7#11 C7b5 C7#5 C9#11
|
||||||
|
C9b5 C9#5 C7b13 C7#9#5 C7#9b5
|
||||||
|
C7#9#11 C7b9#11 C7b9b5 C7b9#5
|
||||||
|
C7b9#9 C7b9b13 C7alt C13#11
|
||||||
|
C13b9 C13#9 C7b9sus C7susadd3
|
||||||
|
C9sus C13sus C7b13sus C Caug
|
||||||
|
CM Cm CM7 Cm7 CM9 CM13 CM7#11
|
||||||
|
CM9#11 CM7#5 Cm6 Cm69 Cm^7
|
||||||
|
C-M7 Cm^9 C-M9 Cm9 Cmadd9
|
||||||
|
Cm11 Cm7b5 Cmb6 Cm#5
|
||||||
|
>\`).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:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`anchor("<c4 g4 c5 g5>").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`:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`mode("<below above duck root>").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:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`mode("<below above duck root>:c5").chord("C").voicing().room(.5)`} punchcard />
|
||||||
|
|
||||||
|
## n
|
||||||
|
|
||||||
|
The `n` control can be used with `voicing` to select individual notes:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`n("0 3 1 2").chord("<C <Fm Db>>").voicing()
|
||||||
|
.clip("4 3 2 1").room(.5)`}
|
||||||
|
punchcard
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Here's an example of a Jazz Blues in F:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`let chords = chord(\`<
|
||||||
|
F7 Bb7 F7 [Cm7 F7]
|
||||||
|
Bb7 Bo F7 [Am7 D7]
|
||||||
|
Gm7 C7 [F7 D7] [Gm7 C7]
|
||||||
|
>\`)
|
||||||
|
$: 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue