Merge pull request #647 from tidalcycles/tonleiter

stateless voicings + tonleiter lib
This commit is contained in:
Felix Roos 2023-07-17 23:34:33 +02:00 committed by GitHub
commit 8583ed0e5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 3627 additions and 3185 deletions

View File

@ -509,7 +509,7 @@ const generic_params = [
* @superDirtOnly
*/
['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'],
// TODO: example
/**
@ -573,6 +573,14 @@ const generic_params = [
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['velocity'],
['voice'], // TODO: synth param
// voicings // https://github.com/tidalcycles/strudel/issues/506
['chord'], // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
['dictionary', 'dict'], // which dictionary to use for the voicings
['anchor'], // the top note to align the voicing to, defaults to c5
['offset'], // how the voicing is offset from the anchored position
[['mode', 'anchor']], // below = anchor note will be removed from the voicing, useful for melody harmonization
/**
* Sets the level of reverb.
*

View File

@ -246,7 +246,7 @@ export function pianoroll({
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const isActive = event.whole.begin <= time && event.endClipped > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;

View File

@ -11,21 +11,24 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
return [pc, acc, oct ? Number(oct) : undefined];
};
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
// turns the given note into its midi number representation
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {

View File

@ -0,0 +1,152 @@
/*
tonleiter.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonleiter.test.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 { describe, test, expect } from 'vitest';
import {
Step,
Note,
transpose,
pc2chroma,
rotateChroma,
chroma2pc,
tokenizeChord,
note2pc,
note2oct,
midi2note,
renderVoicing,
scaleStep,
} from '../tonleiter.mjs';
describe('tonleiter', () => {
test('Step ', () => {
expect(Step.tokenize('#11')).toEqual(['#', 11]);
expect(Step.tokenize('b13')).toEqual(['b', 13]);
expect(Step.tokenize('bb6')).toEqual(['bb', 6]);
expect(Step.tokenize('b3')).toEqual(['b', 3]);
expect(Step.tokenize('3')).toEqual(['', 3]);
expect(Step.tokenize('10')).toEqual(['', 10]);
// expect(Step.tokenize('asdasd')).toThrow();
expect(Step.accidentals('b3')).toEqual(-1);
expect(Step.accidentals('#11')).toEqual(1);
});
test('Note', () => {
expect(Note.tokenize('C##')).toEqual(['C', '##']);
expect(Note.tokenize('Bb')).toEqual(['B', 'b']);
expect(Note.accidentals('C#')).toEqual(1);
expect(Note.accidentals('C##')).toEqual(2);
expect(Note.accidentals('Eb')).toEqual(-1);
expect(Note.accidentals('Bbb')).toEqual(-2);
});
test('transpose', () => {
expect(transpose('F#', '3')).toEqual('A#');
expect(transpose('C', '3')).toEqual('E');
expect(transpose('D', '3')).toEqual('F#');
expect(transpose('E', '3')).toEqual('G#');
expect(transpose('Eb', '3')).toEqual('G');
expect(transpose('Ebb', '3')).toEqual('Gb');
});
test('pc2chroma', () => {
expect(pc2chroma('C')).toBe(0);
expect(pc2chroma('C#')).toBe(1);
expect(pc2chroma('C##')).toBe(2);
expect(pc2chroma('D')).toBe(2);
expect(pc2chroma('Db')).toBe(1);
expect(pc2chroma('Dbb')).toBe(0);
expect(pc2chroma('bb')).toBe(10);
expect(pc2chroma('f')).toBe(5);
expect(pc2chroma('c')).toBe(0);
});
test('rotateChroma', () => {
expect(rotateChroma(0, 1)).toBe(1);
expect(rotateChroma(0, -1)).toBe(11);
expect(rotateChroma(11, 1)).toBe(0);
expect(rotateChroma(11, 13)).toBe(0);
});
test('chroma2pc', () => {
expect(chroma2pc(0)).toBe('C');
expect(chroma2pc(1)).toBe('Db');
expect(chroma2pc(1, true)).toBe('C#');
expect(chroma2pc(2)).toBe('D');
expect(chroma2pc(3)).toBe('Eb');
});
test('tokenizeChord', () => {
expect(tokenizeChord('Cm7')).toEqual(['C', 'm7', undefined]);
expect(tokenizeChord('C#m7')).toEqual(['C#', 'm7', undefined]);
expect(tokenizeChord('Bb^7')).toEqual(['Bb', '^7', undefined]);
expect(tokenizeChord('Bb^7/F')).toEqual(['Bb', '^7', 'F']);
});
test('note2pc', () => {
expect(note2pc('C5')).toBe('C');
expect(note2pc('C52')).toBe('C');
expect(note2pc('Bb3')).toBe('Bb');
expect(note2pc('F')).toBe('F');
});
test('note2oct', () => {
expect(note2oct('C5')).toBe(5);
expect(note2oct('Bb3')).toBe(3);
expect(note2oct('C7')).toBe(7);
expect(note2oct('C10')).toBe(10);
});
test('midi2note', () => {
expect(midi2note(60)).toBe('C4');
expect(midi2note(61)).toBe('Db4');
expect(midi2note(61, true)).toBe('C#4');
});
test('scaleStep', () => {
expect(scaleStep([60, 63, 67], 0)).toBe(60);
expect(scaleStep([60, 63, 67], 1)).toBe(63);
expect(scaleStep([60, 63, 67], 2)).toBe(67);
expect(scaleStep([60, 63, 67], 3)).toBe(72);
expect(scaleStep([60, 63, 67], 4)).toBe(75);
expect(scaleStep([60, 63, 67], -1)).toBe(55);
expect(scaleStep([60, 63, 67], -2)).toBe(51);
expect(scaleStep([60, 63, 67], -3)).toBe(48);
expect(scaleStep([60, 63, 67], -4)).toBe(43);
});
test('renderVoicing', () => {
const dictionary = {
m7: [
'3 7 10 14', // b3 5 b7 9
'10 14 15 19', // b7 9 b3 5
],
};
expect(renderVoicing({ chord: 'Em7', anchor: 'Bb4', dictionary, mode: 'below' })).toEqual([
'G3',
'B3',
'D4',
'Gb4',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'D5', dictionary, mode: 'below' })).toEqual([
'Eb4',
'G4',
'Bb4',
'D5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'G5', dictionary, mode: 'below' })).toEqual([
'Bb4',
'D5',
'Eb5',
'G5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below' })).toEqual([
'Bb4',
'D5',
'Eb5',
'G5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 0 })).toEqual([70]); // Bb4
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 1 })).toEqual([74]); // D5
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 4 })).toEqual([82]); // Bb5
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', offset: 1 })).toEqual([
'Eb5',
'G5',
'Bb5',
'D6',
]);
// expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']);
// TODO: test with offset
});
});

View File

@ -127,18 +127,18 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
*
* The root note defaults to octave 3, if no octave number is given.
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* "0 2 4 6 4 2".scale("C2:major").note()
* n("0 2 4 6 4 2").scale("C:major")
* @example
* "0 2 4 6 4 2"
* .scale("C2:<major minor>")
* .note()
* n("[0,7] 4 [2,7] 4")
* .scale("C:<major minor>/2")
* .s("piano")
* @example
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
* n(rand.range(0,12).segment(8).round())
* .scale("C:ritusen")
* .s("folkharp")
*/

View File

@ -0,0 +1,190 @@
import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel.cycles/core';
import { Interval } from '@tonaljs/tonal';
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
const flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
const pcs = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b'];
const sharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const accs = { b: -1, '#': 1 };
export const pc2chroma = (pc) => {
const [letter, ...rest] = pc.split('');
return pcs.indexOf(letter.toLowerCase()) + rest.reduce((sum, sign) => sum + accs[sign], 0);
};
export const rotateChroma = (chroma, steps) => (chroma + (steps % 12) + 12) % 12;
export const chroma2pc = (chroma, sharp = false) => {
return (sharp ? sharps : flats)[chroma];
};
export function tokenizeChord(chord) {
const match = (chord || '').match(/^([A-G][b#]*)([^/]*)[/]?([A-G][b#]*)?$/);
if (!match) {
// console.warn('could not tokenize chord', chord);
return [];
}
return match.slice(1);
}
export const note2pc = (note) => note.match(/^[A-G][#b]?/i)[0];
export const note2oct = (note) => tokenizeNote(note)[2];
export const note2chroma = (note) => {
return pc2chroma(note2pc(note));
};
// TODO: test
export const midi2chroma = (midi) => midi % 12;
// TODO: test and use in voicing function
export const pitch2chroma = (x, defaultOctave) => {
if (isNoteWithOctave(x)) {
return note2chroma(x);
}
if (isNote(x)) {
//pc
return pc2chroma(x, defaultOctave);
}
if (typeof x === 'number') {
// expect midi
return midi2chroma(x);
}
};
export const step2semitones = (x) => {
let num = Number(x);
if (!isNaN(num)) {
return num;
}
return Interval.semitones(x);
};
export const x2midi = (x) => {
if (typeof x === 'number') {
return x;
}
if (typeof x === 'string') {
return noteToMidi(x);
}
};
// duplicate: util.mjs (does not support sharp flag)
export const midi2note = (midi, sharp = false) => {
const oct = Math.floor(midi / 12) - 1;
const pc = (sharp ? sharps : flats)[midi % 12];
return pc + oct;
};
export function scaleStep(notes, offset) {
notes = notes.map((note) => (typeof note === 'string' ? noteToMidi(note) : note));
const octOffset = Math.floor(offset / notes.length) * 12;
offset = _mod(offset, 12);
return notes[offset % notes.length] + octOffset;
}
// different ways to resolve the note to compare the anchor to (see renderVoicing)
let modeTarget = {
below: (v) => v.slice(-1)[0],
duck: (v) => v.slice(-1)[0],
above: (v) => v[0],
};
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5' }) {
const [root, symbol] = tokenizeChord(chord);
const rootChroma = pc2chroma(root);
anchor = anchor?.note || anchor;
const anchorChroma = pitch2chroma(anchor);
const voicings = dictionary[symbol].map((voicing) =>
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
);
let minDistance, bestIndex;
// calculate distances up from voicing top notes
let chromaDiffs = voicings.map((v, i) => {
const targetStep = modeTarget[mode](v);
const diff = _mod(anchorChroma - targetStep - rootChroma, 12);
if (minDistance === undefined || diff < minDistance) {
minDistance = diff;
bestIndex = i;
}
return diff;
});
const octDiff = Math.ceil(offset / voicings.length) * 12;
const indexWithOffset = _mod(bestIndex + offset, voicings.length);
const voicing = voicings[indexWithOffset];
const targetStep = modeTarget[mode](voicing);
const anchorMidi = noteToMidi(anchor, 4) - chromaDiffs[indexWithOffset] + octDiff;
const voicingMidi = voicing.map((v) => anchorMidi - targetStep + v);
let notes = voicingMidi.map((n) => midi2note(n));
if (mode === 'duck') {
notes = notes.filter((_, i) => voicingMidi[i] !== noteToMidi(anchor));
}
if (n !== undefined) {
return [scaleStep(notes, n)];
}
return notes;
}
// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs
const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7];
const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B'];
const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
export const accidentalOffset = (accidentals) => {
return accidentals.split('#').length - accidentals.split('b').length;
};
const accidentalString = (offset) => {
if (offset < 0) {
return 'b'.repeat(-offset);
}
if (offset > 0) {
return '#'.repeat(offset);
}
return '';
};
export const Step = {
tokenize(step) {
const matches = step.match(/^([#b]*)([1-9][0-9]*)$/);
if (!matches) {
throw new Error(`Step.tokenize: not a valid step: ${step}`);
}
const [accidentals, stepNumber] = matches.slice(1);
return [accidentals, parseInt(stepNumber)];
},
accidentals(step) {
return accidentalOffset(Step.tokenize(step)[0]);
},
};
export const Note = {
// TODO: support octave numbers
tokenize(note) {
return [note[0], note.slice(1)];
},
accidentals(note) {
return accidentalOffset(this.tokenize(note)[1]);
},
};
// TODO: support octave numbers
export function transpose(note, step) {
// example: E, 3
const stepNumber = Step.tokenize(step)[1]; // 3
const noteLetter = Note.tokenize(note)[0]; // E
const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2"
const targetNote = noteLetters[(noteIndex + stepNumber - 1) % 8]; // G "G is a third above E"
const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C"
const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C"
const indexOffset = targetIndex - rootIndex; // 3 (E to G is normally a 3 semitones)
const stepIndex = steps.indexOf(stepNumber); // 4 ("3" is normally 4 semitones)
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
return [targetNote, offsetAccidentals].join('');
}
//Note("Bb3").transpose("c3")

View File

@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { stack, register } from '@strudel.cycles/core';
import { renderVoicing } from './tonleiter.mjs';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
@ -49,10 +50,33 @@ const triads = {
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
const defaultDictionary = {
// triads
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
// sevenths chords
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
triads: { dictionary: triads },
guidetones: { dictionary: guidetones },
lefthand: { dictionary: lefthand, range: ['F3', 'A4'], mode: 'below', anchor: 'a4' },
triads: { dictionary: triads, mode: 'below', anchor: 'a4' },
guidetones: { dictionary: guidetones, mode: 'above', anchor: 'a4' },
default: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
@ -81,6 +105,11 @@ export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
Object.assign(voicingRegistry, { [name]: { dictionary, range } });
};
// new call signature
export const registerVoicings = (name, dictionary, options = {}) => {
Object.assign(voicingRegistry, { [name]: { dictionary, ...options } });
};
const getVoicing = (chord, dictionaryName, lastVoicing) => {
const { dictionary, range } = voicingRegistry[dictionaryName];
return dictionaryVoicing({
@ -93,6 +122,7 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
};
/**
* DEPRECATED: still works, but it is recommended you use .voicing instead (without s).
* Turns chord symbols into voicings, using the smoothest voice leading possible.
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
*
@ -129,7 +159,50 @@ export const voicings = register('voicings', function (dictionary, pat) {
*/
export const rootNotes = register('rootNotes', function (octave, pat) {
return pat.fmap((value) => {
const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
return root + octave;
const chord = value.chord || value;
const root = chord.match(/^([a-gA-G][b#]?).*$/)[1];
const note = root + octave;
return value.chord ? { note } : note;
});
});
/**
* Turns chord symbols into voicings. You can use the following control params:
*
* - `chord`: Note, followed by chord symbol, e.g. C Am G7 Bb^7
* - `dict`: voicing dictionary to use, falls back to default dictionary
* - `anchor`: the note that is used to align the chord
* - `mode`: how the voicing is aligned to the anchor
* - `below`: top note <= anchor
* - `duck`: top note <= anchor, anchor excluded
* - `above`: bottom note >= anchor
* - `offset`: whole number that shifts the voicing up or down to the next voicing
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
*
* All of the above controls are optional, except `chord`.
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
*
* @name voicing
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
* @example
* voicing("<C Am F G>")
* @example
* n("0 1 2 3 4 5 6 7").chord("<C Am F G>").voicing()
*/
export const voicing = register('voicing', function (pat) {
return pat
.fmap((value) => {
// destructure voicing controls out
value = typeof value === 'string' ? { chord: value } : value;
let { dictionary = 'default', chord, anchor, offset, mode, n, ...rest } = value;
dictionary =
typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' };
let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n });
return stack(...notes)
.note()
.set(rest); // rest does not include voicing controls anymore!
})
.outerJoin();
});

View File

@ -3545,96 +3545,96 @@ exports[`runs examples > example "saw" example index 1 1`] = `
exports[`runs examples > example "scale" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:C2 ]",
"[ 1/6 → 1/3 | note:E2 ]",
"[ 1/3 → 1/2 | note:G2 ]",
"[ 1/2 → 2/3 | note:B2 ]",
"[ 2/3 → 5/6 | note:G2 ]",
"[ 5/6 → 1/1 | note:E2 ]",
"[ 1/1 → 7/6 | note:C2 ]",
"[ 7/6 → 4/3 | note:E2 ]",
"[ 4/3 → 3/2 | note:G2 ]",
"[ 3/2 → 5/3 | note:B2 ]",
"[ 5/3 → 11/6 | note:G2 ]",
"[ 11/6 → 2/1 | note:E2 ]",
"[ 2/1 → 13/6 | note:C2 ]",
"[ 13/6 → 7/3 | note:E2 ]",
"[ 7/3 → 5/2 | note:G2 ]",
"[ 5/2 → 8/3 | note:B2 ]",
"[ 8/3 → 17/6 | note:G2 ]",
"[ 17/6 → 3/1 | note:E2 ]",
"[ 3/1 → 19/6 | note:C2 ]",
"[ 19/6 → 10/3 | note:E2 ]",
"[ 10/3 → 7/2 | note:G2 ]",
"[ 7/2 → 11/3 | note:B2 ]",
"[ 11/3 → 23/6 | note:G2 ]",
"[ 23/6 → 4/1 | note:E2 ]",
"[ 0/1 → 1/6 | n:0 note:C3 ]",
"[ 1/6 → 1/3 | n:2 note:E3 ]",
"[ 1/3 → 1/2 | n:4 note:G3 ]",
"[ 1/2 → 2/3 | n:6 note:B3 ]",
"[ 2/3 → 5/6 | n:4 note:G3 ]",
"[ 5/6 → 1/1 | n:2 note:E3 ]",
"[ 1/1 → 7/6 | n:0 note:C3 ]",
"[ 7/6 → 4/3 | n:2 note:E3 ]",
"[ 4/3 → 3/2 | n:4 note:G3 ]",
"[ 3/2 → 5/3 | n:6 note:B3 ]",
"[ 5/3 → 11/6 | n:4 note:G3 ]",
"[ 11/6 → 2/1 | n:2 note:E3 ]",
"[ 2/1 → 13/6 | n:0 note:C3 ]",
"[ 13/6 → 7/3 | n:2 note:E3 ]",
"[ 7/3 → 5/2 | n:4 note:G3 ]",
"[ 5/2 → 8/3 | n:6 note:B3 ]",
"[ 8/3 → 17/6 | n:4 note:G3 ]",
"[ 17/6 → 3/1 | n:2 note:E3 ]",
"[ 3/1 → 19/6 | n:0 note:C3 ]",
"[ 19/6 → 10/3 | n:2 note:E3 ]",
"[ 10/3 → 7/2 | n:4 note:G3 ]",
"[ 7/2 → 11/3 | n:6 note:B3 ]",
"[ 11/3 → 23/6 | n:4 note:G3 ]",
"[ 23/6 → 4/1 | n:2 note:E3 ]",
]
`;
exports[`runs examples > example "scale" example index 1 1`] = `
[
"[ 0/1 → 1/6 | note:C2 ]",
"[ 1/6 → 1/3 | note:E2 ]",
"[ 1/3 → 1/2 | note:G2 ]",
"[ 1/2 → 2/3 | note:B2 ]",
"[ 2/3 → 5/6 | note:G2 ]",
"[ 5/6 → 1/1 | note:E2 ]",
"[ 1/1 → 7/6 | note:C2 ]",
"[ 7/6 → 4/3 | note:Eb2 ]",
"[ 4/3 → 3/2 | note:G2 ]",
"[ 3/2 → 5/3 | note:Bb2 ]",
"[ 5/3 → 11/6 | note:G2 ]",
"[ 11/6 → 2/1 | note:Eb2 ]",
"[ 2/1 → 13/6 | note:C2 ]",
"[ 13/6 → 7/3 | note:E2 ]",
"[ 7/3 → 5/2 | note:G2 ]",
"[ 5/2 → 8/3 | note:B2 ]",
"[ 8/3 → 17/6 | note:G2 ]",
"[ 17/6 → 3/1 | note:E2 ]",
"[ 3/1 → 19/6 | note:C2 ]",
"[ 19/6 → 10/3 | note:Eb2 ]",
"[ 10/3 → 7/2 | note:G2 ]",
"[ 7/2 → 11/3 | note:Bb2 ]",
"[ 11/3 → 23/6 | note:G2 ]",
"[ 23/6 → 4/1 | note:Eb2 ]",
"[ 0/1 → 1/4 | n:0 note:C3 s:piano ]",
"[ 0/1 → 1/4 | n:7 note:C4 s:piano ]",
"[ 1/4 → 1/2 | n:4 note:G3 s:piano ]",
"[ 1/2 → 3/4 | n:2 note:E3 s:piano ]",
"[ 1/2 → 3/4 | n:7 note:C4 s:piano ]",
"[ 3/4 → 1/1 | n:4 note:G3 s:piano ]",
"[ 1/1 → 5/4 | n:0 note:C3 s:piano ]",
"[ 1/1 → 5/4 | n:7 note:C4 s:piano ]",
"[ 5/4 → 3/2 | n:4 note:G3 s:piano ]",
"[ 3/2 → 7/4 | n:2 note:E3 s:piano ]",
"[ 3/2 → 7/4 | n:7 note:C4 s:piano ]",
"[ 7/4 → 2/1 | n:4 note:G3 s:piano ]",
"[ 2/1 → 9/4 | n:0 note:C3 s:piano ]",
"[ 2/1 → 9/4 | n:7 note:C4 s:piano ]",
"[ 9/4 → 5/2 | n:4 note:G3 s:piano ]",
"[ 5/2 → 11/4 | n:2 note:Eb3 s:piano ]",
"[ 5/2 → 11/4 | n:7 note:C4 s:piano ]",
"[ 11/4 → 3/1 | n:4 note:G3 s:piano ]",
"[ 3/1 → 13/4 | n:0 note:C3 s:piano ]",
"[ 3/1 → 13/4 | n:7 note:C4 s:piano ]",
"[ 13/4 → 7/2 | n:4 note:G3 s:piano ]",
"[ 7/2 → 15/4 | n:2 note:Eb3 s:piano ]",
"[ 7/2 → 15/4 | n:7 note:C4 s:piano ]",
"[ 15/4 → 4/1 | n:4 note:G3 s:piano ]",
]
`;
exports[`runs examples > example "scale" example index 2 1`] = `
[
"[ 0/1 → 1/8 | note:C3 s:folkharp ]",
"[ 1/8 → 1/4 | note:B2 s:folkharp ]",
"[ 1/4 → 3/8 | note:A2 s:folkharp ]",
"[ 3/8 → 1/2 | note:G2 s:folkharp ]",
"[ 1/2 → 5/8 | note:F2 s:folkharp ]",
"[ 5/8 → 3/4 | note:E2 s:folkharp ]",
"[ 3/4 → 7/8 | note:D2 s:folkharp ]",
"[ 7/8 → 1/1 | note:C2 s:folkharp ]",
"[ 1/1 → 9/8 | note:C3 s:folkharp ]",
"[ 9/8 → 5/4 | note:Bb2 s:folkharp ]",
"[ 5/4 → 11/8 | note:Ab2 s:folkharp ]",
"[ 11/8 → 3/2 | note:G2 s:folkharp ]",
"[ 3/2 → 13/8 | note:F2 s:folkharp ]",
"[ 13/8 → 7/4 | note:Eb2 s:folkharp ]",
"[ 7/4 → 15/8 | note:D2 s:folkharp ]",
"[ 15/8 → 2/1 | note:C2 s:folkharp ]",
"[ 2/1 → 17/8 | note:C3 s:folkharp ]",
"[ 17/8 → 9/4 | note:B2 s:folkharp ]",
"[ 9/4 → 19/8 | note:A2 s:folkharp ]",
"[ 19/8 → 5/2 | note:G2 s:folkharp ]",
"[ 5/2 → 21/8 | note:F2 s:folkharp ]",
"[ 21/8 → 11/4 | note:E2 s:folkharp ]",
"[ 11/4 → 23/8 | note:D2 s:folkharp ]",
"[ 23/8 → 3/1 | note:C2 s:folkharp ]",
"[ 3/1 → 25/8 | note:C3 s:folkharp ]",
"[ 25/8 → 13/4 | note:Bb2 s:folkharp ]",
"[ 13/4 → 27/8 | note:Ab2 s:folkharp ]",
"[ 27/8 → 7/2 | note:G2 s:folkharp ]",
"[ 7/2 → 29/8 | note:F2 s:folkharp ]",
"[ 29/8 → 15/4 | note:Eb2 s:folkharp ]",
"[ 15/4 → 31/8 | note:D2 s:folkharp ]",
"[ 31/8 → 4/1 | note:C2 s:folkharp ]",
"[ 0/1 → 1/8 | n:10 note:C5 s:folkharp ]",
"[ 1/8 → 1/4 | n:2 note:F3 s:folkharp ]",
"[ 1/4 → 3/8 | n:7 note:F4 s:folkharp ]",
"[ 3/8 → 1/2 | n:4 note:A3 s:folkharp ]",
"[ 1/2 → 5/8 | n:2 note:F3 s:folkharp ]",
"[ 5/8 → 3/4 | n:5 note:C4 s:folkharp ]",
"[ 3/4 → 7/8 | n:9 note:A4 s:folkharp ]",
"[ 7/8 → 1/1 | n:8 note:G4 s:folkharp ]",
"[ 1/1 → 9/8 | n:7 note:F4 s:folkharp ]",
"[ 9/8 → 5/4 | n:1 note:D3 s:folkharp ]",
"[ 5/4 → 11/8 | n:1 note:D3 s:folkharp ]",
"[ 11/8 → 3/2 | n:6 note:D4 s:folkharp ]",
"[ 3/2 → 13/8 | n:2 note:F3 s:folkharp ]",
"[ 13/8 → 7/4 | n:4 note:A3 s:folkharp ]",
"[ 7/4 → 15/8 | n:6 note:D4 s:folkharp ]",
"[ 15/8 → 2/1 | n:10 note:C5 s:folkharp ]",
"[ 2/1 → 17/8 | n:4 note:A3 s:folkharp ]",
"[ 17/8 → 9/4 | n:0 note:C3 s:folkharp ]",
"[ 9/4 → 19/8 | n:8 note:G4 s:folkharp ]",
"[ 19/8 → 5/2 | n:2 note:F3 s:folkharp ]",
"[ 5/2 → 21/8 | n:7 note:F4 s:folkharp ]",
"[ 21/8 → 11/4 | n:6 note:D4 s:folkharp ]",
"[ 11/4 → 23/8 | n:11 note:D5 s:folkharp ]",
"[ 23/8 → 3/1 | n:3 note:G3 s:folkharp ]",
"[ 3/1 → 25/8 | n:0 note:C3 s:folkharp ]",
"[ 25/8 → 13/4 | n:11 note:D5 s:folkharp ]",
"[ 13/4 → 27/8 | n:4 note:A3 s:folkharp ]",
"[ 27/8 → 7/2 | n:9 note:A4 s:folkharp ]",
"[ 7/2 → 29/8 | n:10 note:C5 s:folkharp ]",
"[ 29/8 → 15/4 | n:12 note:F5 s:folkharp ]",
"[ 15/4 → 31/8 | n:1 note:D3 s:folkharp ]",
"[ 31/8 → 4/1 | n:4 note:A3 s:folkharp ]",
]
`;
@ -4431,6 +4431,60 @@ exports[`runs examples > example "velocity" example index 0 1`] = `
]
`;
exports[`runs examples > example "voicing" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:G4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:A4 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 2/1 → 3/1 | note:F4 ]",
"[ 2/1 → 3/1 | note:A4 ]",
"[ 2/1 → 3/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:D4 ]",
"[ 3/1 → 4/1 | note:G4 ]",
"[ 3/1 → 4/1 | note:B4 ]",
]
`;
exports[`runs examples > example "voicing" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:64 ]",
"[ 1/8 → 1/4 | note:67 ]",
"[ 1/4 → 3/8 | note:72 ]",
"[ 3/8 → 1/2 | note:76 ]",
"[ 1/2 → 5/8 | note:79 ]",
"[ 5/8 → 3/4 | note:84 ]",
"[ 3/4 → 7/8 | note:88 ]",
"[ 7/8 → 1/1 | note:91 ]",
"[ 1/1 → 9/8 | note:64 ]",
"[ 9/8 → 5/4 | note:69 ]",
"[ 5/4 → 11/8 | note:72 ]",
"[ 11/8 → 3/2 | note:76 ]",
"[ 3/2 → 13/8 | note:81 ]",
"[ 13/8 → 7/4 | note:84 ]",
"[ 7/4 → 15/8 | note:88 ]",
"[ 15/8 → 2/1 | note:93 ]",
"[ 2/1 → 17/8 | note:65 ]",
"[ 17/8 → 9/4 | note:69 ]",
"[ 9/4 → 19/8 | note:72 ]",
"[ 19/8 → 5/2 | note:77 ]",
"[ 5/2 → 21/8 | note:81 ]",
"[ 21/8 → 11/4 | note:84 ]",
"[ 11/4 → 23/8 | note:89 ]",
"[ 23/8 → 3/1 | note:93 ]",
"[ 3/1 → 25/8 | note:62 ]",
"[ 25/8 → 13/4 | note:67 ]",
"[ 13/4 → 27/8 | note:71 ]",
"[ 27/8 → 7/2 | note:74 ]",
"[ 7/2 → 29/8 | note:79 ]",
"[ 29/8 → 15/4 | note:83 ]",
"[ 15/4 → 31/8 | note:86 ]",
"[ 31/8 → 4/1 | note:91 ]",
]
`;
exports[`runs examples > example "voicings" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:B3 ]",
@ -4469,31 +4523,6 @@ exports[`runs examples > example "vowel" example index 0 1`] = `
]
`;
exports[`runs examples > example "webdirt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd n:0 ]",
"[ 1/8 → 1/4 | s:bd n:0 ]",
"[ 1/4 → 1/2 | s:hh n:0 ]",
"[ 1/2 → 3/4 | s:sd n:0 ]",
"[ 3/4 → 1/1 | s:hh n:0 ]",
"[ 1/1 → 9/8 | s:bd n:1 ]",
"[ 9/8 → 5/4 | s:bd n:1 ]",
"[ 5/4 → 3/2 | s:hh n:1 ]",
"[ 3/2 → 7/4 | s:sd n:1 ]",
"[ 7/4 → 2/1 | s:hh n:1 ]",
"[ 2/1 → 17/8 | s:bd n:0 ]",
"[ 17/8 → 9/4 | s:bd n:0 ]",
"[ 9/4 → 5/2 | s:hh n:0 ]",
"[ 5/2 → 11/4 | s:sd n:0 ]",
"[ 11/4 → 3/1 | s:hh n:0 ]",
"[ 3/1 → 25/8 | s:bd n:1 ]",
"[ 25/8 → 13/4 | s:bd n:1 ]",
"[ 13/4 → 7/2 | s:hh n:1 ]",
"[ 7/2 → 15/4 | s:sd n:1 ]",
"[ 15/4 → 4/1 | s:hh n:1 ]",
]
`;
exports[`runs examples > example "when" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c3 ]",

File diff suppressed because it is too large Load Diff

View File

@ -10,20 +10,13 @@ import { JsDoc } from '../../docs/JsDoc';
These functions use [tonaljs](https://github.com/tonaljs/tonal) to provide helpers for musical operations.
### voicing()
<JsDoc client:idle name="voicing" h={0} />
### scale(name)
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranspose.
<MiniRepl
client:idle
tune={`"0 2 4 6 4 2"
.scale("C2:major C2:minor").slow(2))
.note().s("piano")`}
/>
Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3.
All the available scale names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
<JsDoc client:idle name="scale" h={0} />
### transpose(semitones)

View File

@ -68,32 +68,35 @@ stack(
`;
export const giantSteps = `// John Coltrane - Giant Steps
setVoicingRange('lefthand', ['E3', 'G4']);
let melody = note(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 F#5]",
)
stack(
// melody
seq(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 F#5]",
).color('#F8E71C'),
melody.color('#F8E71C'),
// chords
seq(
chord(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
).voicings('lefthand').color('#7ED321'),
).dict('lefthand')
.anchor(melody).mode('duck')
.voicing().color('#7ED321'),
// bass
seq(
note(
"[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
).color('#00B8D4')
).slow(20).note()
//.pianoroll({fold:1})`;
).slow(20)
.pianoroll({fold:1})`;
export const zeldasRescue = `// Koji Kondo - Princess Zelda's Rescue
stack(
@ -134,28 +137,31 @@ const drums = stack(
s("[~ hh]*2").delay(.3).delayfeedback(.5).delaytime(.125).gain(.4)
);
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
const synths = stack(
"<eb4 d4 c4 b3>/2"
.scale(timeCat([3,'C minor'],[1,'C melodic minor'])
.slow(8)).struct("[~ x]*2")
.scale("<C:minor!3 C:melodic:minor>/2")
.struct("[~ x]*2")
.layer(
x=>x.scaleTranspose(0).early(0),
x=>x.scaleTranspose(2).early(1/8),
x=>x.scaleTranspose(7).early(1/4),
x=>x.scaleTranspose(8).early(3/8)
).apply(thru).note().apply(keys).mask("<~ x>/16")
).note().apply(keys).mask("<~ x>/16")
.color('darkseagreen'),
note("<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".apply(thru))
note("<C2 Bb1 Ab1 [G1 [G2 G1]]>/2")
.struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2))
.s('sawtooth').attack(0.001).decay(0.2).sustain(1).cutoff(500)
.color('brown'),
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.2 ~]".fast(2))
.voicings('lefthand')
.apply(thru).every(2, early(1/8)).note().apply(keys).sustain(0)
chord("<Cm7 Bb7 Fm7 G7b13>/2")
.struct("~ [x@0.2 ~]".fast(2))
.dict('lefthand').voicing()
.every(2, early(1/8))
.apply(keys).sustain(0)
.delay(.4).delaytime(.12)
.mask("<x@7 ~>/8".early(1/4))
)
).add(note("<-1 0>/8"))
stack(
drums.fast(2).color('tomato'),
synths
@ -263,15 +269,20 @@ export const festivalOfFingers = `// "Festival of fingers"
const chords = "<Cm7 Fm7 G7 F#7>";
stack(
chords.voicings('lefthand').struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)),
chords.rootNotes(2).struct("x(4,8,-2)"),
chord(chords).dict('lefthand').voicing().struct("x(3,8,-1)")
.velocity(.5).off(1/7,x=>x.add(note(12)).velocity(.2)),
chords.rootNotes(2).struct("x(4,8,-2)").note(),
chords.rootNotes(4)
.scale(cat('C minor','F dorian','G dorian','F# mixolydian'))
.struct("x(3,8,-2)".fast(2))
.scaleTranspose("0 4 0 6".early(".125 .5")).layer(scaleTranspose("0,<2 [4,6] [5,7]>/4"))
.note()
).slow(2)
.velocity(sine.struct("x*8").add(3/5).mul(2/5).fast(8))
.note().piano()`;
.piano()`;
// iter, echo, echoWith
export const undergroundPlumber = `// "Underground plumber"
@ -527,10 +538,10 @@ stack(
.gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
//.hush()
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation
.note() // wrap in "note"
,chord("<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>")
.dict('lefthand').voicing() // chords
.add(note("0,.04")) // add second, slightly detuned voice
.add(note(perlin.range(0,.5))) // random pitch variation
.s('sawtooth') // waveform
.gain(.16) // turn down
.cutoff(500) // fixed cutoff
@ -559,14 +570,15 @@ samples({
perc: ['perc/002_perc2.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');
"C^7 Am7 Dm7 G7".slow(2).voicings('lefthand')
.stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major'))
.note().slow(4)
chord("<C^7 Am7 Dm7 G7>*2").dict('lefthand').anchor("G4").voicing()
.stack(n("0@6 [<1 2> <2 0> 1]@2").scale('C5 major'))
.slow(4)
.s("gm_epiano1:1")
.color('steelblue')
.stack(
"<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2".scale('C3 major')
.note().s('sawtooth').color('brown')
n("<-7 ~@2 [~@2 -7] -9 ~@2 [~@2 -9] -10!2 ~ [~@2 -10] -5 ~ [-3 -2 -10]@2>*2")
.scale('C3 major')
.s('sawtooth').color('brown')
)
.attack(0.05).decay(.1).sustain(.7)
.cutoff(perlin.range(800,2000))
@ -664,8 +676,10 @@ stack(
s("mt lt ht").struct("x(3,8)").fast(2).gain(.5).room(.5).sometimes(x=>x.speed(".5")),
s("misc:2").speed(1).delay(.5).delaytime(1/3).gain(.4),
// chords
note("[~ Gm7] ~ [~ Dm7] ~".voicings('lefthand').superimpose(x=>x.add(.1)))
.s('sawtooth').gain(.5)
chord("[~ Gm7] ~ [~ Dm7] ~")
.dict('lefthand').voicing()
.add(note("0,.1"))
.s('sawtooth').gain(.8)
.cutoff(perlin.range(400,3000).slow(8))
.decay(perlin.range(0.05,.2)).sustain(0)
.delay(.9).room(1),
@ -695,7 +709,11 @@ setVoicingRange('lefthand', ['c3','a4'])
stack(
s('bass').loopAt(8).clip(1),
s("bd*2, ~ sd,hh*4"),
note("Abm7".voicings('lefthand').struct("x(3,8,1)".slow(2))),
chord("Abm7")
.mode("below:G4")
.dict('lefthand')
.voicing()
.struct("x(3,8,1)".slow(2)),
"0 1 2 3".scale('ab4 minor pentatonic')
.superimpose(x=>x.add(.1))
.sometimes(x=>x.add(12))
@ -859,7 +877,7 @@ export const loungeSponge = `// "Lounge sponge"
await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc')
stack(
note("<C^7 A7 Dm7 Fm7>/2".voicings('lefthand'))
chord("<C^7 A7 Dm7 Fm7>/2").dict('lefthand').voicing()
.cutoff(sine.range(500,2000).round().slow(16))
.euclidLegato(3,8).csound('FM1')
,
@ -878,9 +896,11 @@ export const arpoon = `// "Arpoon"
await samples('github:tidalcycles/Dirt-Samples/master')
note("<<Am7 C^7> C7 F^7 [Fm7 E7b9]>".voicings('lefthand'))
.arp("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2)
.add(perlin.range(0,0.2).add("<0 12>/8").note())
n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2)
.offset("<<1 2> 2 1 1>")
.chord("<<Am7 C^7> C7 F^7 [Fm7 E7b9]>")
.dict('lefthand').voicing()
.add(perlin.range(0,0.2).add("<-12 0>/8").note())
.cutoff(perlin.range(500,4000)).resonance(12)
.gain("<.5 .8>*16")
.decay(.16).sustain(0.5)