refactor tonal functions to 'register'

This commit is contained in:
Felix Roos
2022-12-10 21:34:38 +01:00
parent 4f7c013c78
commit bc43fc9585
6 changed files with 84 additions and 63 deletions
+3
View File
@@ -1,2 +1,5 @@
import './tonal.mjs';
import './voicings.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
+10 -14
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { Pattern, _mod } from '@strudel.cycles/core';
import { register, _mod } from '@strudel.cycles/core';
// transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) {
@@ -74,8 +74,8 @@ function scaleOffset(scale, offset, note) {
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/
Pattern.prototype._transpose = function (intervalOrSemitones) {
return this.withHap((hap) => {
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
@@ -87,7 +87,7 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
});
};
});
// example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2))
// TODO: add Pattern.define(name, function, options) that handles all the meta programming stuff
@@ -110,8 +110,8 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
* .note()
*/
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
return this.withHap((hap) => {
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
@@ -120,7 +120,7 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
};
});
/**
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
@@ -141,8 +141,8 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
* .note()
*/
Pattern.prototype._scale = function (scale /* : string */) {
return this.withHap((hap) => {
export const scale = register('scale', function (scale /* : string */, pat) {
return pat.withHap((hap) => {
let note = hap.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
@@ -152,8 +152,4 @@ Pattern.prototype._scale = function (scale /* : string */) {
}
return hap.withValue(() => note).setContext({ ...hap.context, scale });
});
};
Pattern.prototype.define('transpose', (a, pat) => pat.transpose(a), { composable: true, patternified: true });
Pattern.prototype.define('scale', (a, pat) => pat.scale(a), { composable: true, patternified: true });
Pattern.prototype.define('scaleTranspose', (a, pat) => pat.scaleTranspose(a), { composable: true, patternified: true });
});
+58 -36
View File
@@ -4,31 +4,47 @@ 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/>.
*/
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core';
import { stack, register } from '@strudel.cycles/core';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) =>
dictionaryVoicing({
export const voicingDictionaries = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
};
/**
* Adds a new custom voicing dictionary.
*
* @name addVoicings
* @memberof Pattern
* @param {string} name identifier for the voicing dictionary
* @param {Object} dictionary maps chord symbol to possible voicings
* @param {Array} range min, max note
* @returns Pattern
* @example
* addVoicings('cookie', {
* 7: ['3M 7m 9M 12P 15P', '7m 10M 13M 16M 19P'],
* '^7': ['3M 6M 9M 12P 14M', '7M 10M 13M 16M 19P'],
* m7: ['8P 11P 14m 17m 19P', '5P 8P 11P 14m 17m'],
* m7b5: ['3m 5d 8P 11P 14m', '5d 8P 11P 14m 17m'],
* o7: ['3m 6M 9M 11A 15P'],
* '7alt': ['3M 7m 10m 13m 15P'],
* '7#11': ['7m 10m 13m 15P 17m'],
* }, ['C3', 'C6'])
*/
export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
Object.assign(voicingDictionaries, { [name]: { dictionary, range } });
};
const getVoicing = (chord, dictionaryName, lastVoicing) => {
const { dictionary, range } = voicingDictionaries[dictionaryName];
return dictionaryVoicing({
chord,
dictionary: lefthand,
dictionary,
range,
picker: minTopNoteDiff,
lastVoicing,
});
const Pattern = _Pattern;
Pattern.prototype.fmapNested = function (func) {
return new Pattern((span) =>
this.query(span)
.map((event) =>
reify(func(event))
.query(span)
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context)),
)
.flat(),
);
};
/**
@@ -37,32 +53,38 @@ Pattern.prototype.fmapNested = function (func) {
*
* @name voicings
* @memberof Pattern
* @param {range} range note range for possible voicings (optional, defaults to `['F3', 'A4']`)
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
* @example
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()
* stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
*/
Pattern.prototype.voicings = function (range) {
export const voicings = register('voicings', function (dictionary, pat) {
let lastVoicing;
if (!range?.length) {
// allows to pass empty array, if too lazy to specify range
range = ['F3', 'A4'];
}
return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value, lastVoicing, range);
return stack(...lastVoicing).withContext(() => ({
return pat
.fmap((value) => {
lastVoicing = getVoicing(value, dictionary, lastVoicing);
return stack(...lastVoicing);
})
.outerJoin();
/* .withContext(() => ({
locations: event.context.locations || [],
}));
});
};
})); */
});
Pattern.prototype._rootNotes = function (octave = 2) {
return this.fmap((value) => {
/**
* Maps the chords of the incoming pattern to root notes in the given octave.
*
* @name rootNotes
* @memberof Pattern
* @param {octave} octave octave to use
* @returns Pattern
* @example
* "<C^7 A7 Dm7 G7>".rootNotes(2).note()
*/
export const rootNotes = register('rootNotes', function (octave, pat) {
return pat.fmap((value) => {
const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
return root + octave;
});
};
Pattern.prototype.define('voicings', (range, pat) => pat.voicings(range), { composable: true });
Pattern.prototype.define('rootNotes', (oct, pat) => pat.rootNotes(oct), { composable: true, patternified: true });
});