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

View File

@ -1,2 +1,5 @@
import './tonal.mjs'; import './tonal.mjs';
import './voicings.mjs'; import './voicings.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';

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 { 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 // transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) { // 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() * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/ */
Pattern.prototype._transpose = function (intervalOrSemitones) { export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return this.withHap((hap) => { return pat.withHap((hap) => {
const interval = !isNaN(Number(intervalOrSemitones)) const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */) ? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones); : 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 // 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))); 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)) // 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 // TODO: add Pattern.define(name, function, options) that handles all the meta programming stuff
@ -110,8 +110,8 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
* .note() * .note()
*/ */
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) { export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
return this.withHap((hap) => { return pat.withHap((hap) => {
if (!hap.context.scale) { if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .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)); 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}. * 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() * .note()
*/ */
Pattern.prototype._scale = function (scale /* : string */) { export const scale = register('scale', function (scale /* : string */, pat) {
return this.withHap((hap) => { return pat.withHap((hap) => {
let note = hap.value; let note = hap.value;
const asNumber = Number(note); const asNumber = Number(note);
if (!isNaN(asNumber)) { if (!isNaN(asNumber)) {
@ -152,8 +152,4 @@ Pattern.prototype._scale = function (scale /* : string */) {
} }
return hap.withValue(() => note).setContext({ ...hap.context, scale }); 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 });

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/>. 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'; import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) => export const voicingDictionaries = {
dictionaryVoicing({ 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, chord,
dictionary: lefthand, dictionary,
range, range,
picker: minTopNoteDiff, picker: minTopNoteDiff,
lastVoicing, 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 * @name voicings
* @memberof Pattern * @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 * @returns Pattern
* @example * @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; let lastVoicing;
if (!range?.length) { return pat
// allows to pass empty array, if too lazy to specify range .fmap((value) => {
range = ['F3', 'A4']; lastVoicing = getVoicing(value, dictionary, lastVoicing);
} return stack(...lastVoicing);
return this.fmapNested((event) => { })
lastVoicing = getVoicing(event.value, lastVoicing, range); .outerJoin();
return stack(...lastVoicing).withContext(() => ({ /* .withContext(() => ({
locations: event.context.locations || [], 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]; const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
return root + octave; 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 });

View File

@ -108,7 +108,7 @@ export const struct = `stack(
export const magicSofa = `stack( export const magicSofa = `stack(
"<C^7 F^7 ~> <Dm7 G7 A7 ~>" "<C^7 F^7 ~> <Dm7 G7 A7 ~>"
.every(2, fast(2)) .every(2, fast(2))
.voicings(), .voicings('lefthand'),
"<c2 f2 g2> <d2 g2 a2 e2>" "<c2 f2 g2> <d2 g2 a2 e2>"
).transpose("<0 2 3 4>")`; ).transpose("<0 2 3 4>")`;
// below doesn't work anymore due to constructor cleanup // below doesn't work anymore due to constructor cleanup
@ -156,7 +156,7 @@ const synths = stack(
scaleTranspose(8).early(3/8) scaleTranspose(8).early(3/8)
).apply(thru).tone(keys).mask("<~ x>/16"), ).apply(thru).tone(keys).mask("<~ x>/16"),
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).apply(thru).tone(bass), "<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).apply(thru).tone(bass),
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.1 ~]".fast(2)).voicings().apply(thru).every(2, early(1/8)).tone(keys).mask("<x@7 ~>/8".early(1/4)) "<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.1 ~]".fast(2)).voicings('lefthand').apply(thru).every(2, early(1/8)).tone(keys).mask("<x@7 ~>/8".early(1/4))
) )
stack( stack(
drums.fast(2), drums.fast(2),
@ -351,7 +351,7 @@ stack(
export const bossa = `const scales = sequence('C minor', ['D locrian', 'G phrygian'], 'Bb2 minor', ['C locrian','F phrygian']).slow(4) export const bossa = `const scales = sequence('C minor', ['D locrian', 'G phrygian'], 'Bb2 minor', ['C locrian','F phrygian']).slow(4)
stack( stack(
"<Cm7 [Dm7b5 G7b9] Bbm7 [Cm7b5 F7b9]>".fast(2).struct("x ~ x@3 x ~ x ~ ~ ~ x ~ x@3".late(1/8)).early(1/8).slow(2).voicings(), "<Cm7 [Dm7b5 G7b9] Bbm7 [Cm7b5 F7b9]>".fast(2).struct("x ~ x@3 x ~ x ~ ~ ~ x ~ x@3".late(1/8)).early(1/8).slow(2).voicings('lefthand'),
"[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25) "[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25)
).note().piano().slow(2)`; ).note().piano().slow(2)`;

View File

@ -138,7 +138,7 @@ const synths = stack(
note("<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".apply(thru)) note("<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".apply(thru))
.struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)) .struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2))
.s('sawtooth').attack(0.001).decay(0.2).sustain(1).cutoff(500), .s('sawtooth').attack(0.001).decay(0.2).sustain(1).cutoff(500),
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.2 ~]".fast(2)).voicings() "<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) .apply(thru).every(2, early(1/8)).note().apply(keys).sustain(0)
.delay(.4).delaytime(.12) .delay(.4).delaytime(.12)
.mask("<x@7 ~>/8".early(1/4)) .mask("<x@7 ~>/8".early(1/4))
@ -245,7 +245,7 @@ export const festivalOfFingers = `// licensed with CC BY-NC-SA 4.0 https://creat
// by Felix Roos // by Felix Roos
const chords = "<Cm7 Fm7 G7 F#7>"; const chords = "<Cm7 Fm7 G7 F#7>";
stack( stack(
chords.voicings().struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)), 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)"), chords.rootNotes(2).struct("x(4,8,-2)"),
chords.rootNotes(4) chords.rootNotes(4)
.scale(cat('C minor','F dorian','G dorian','F# mixolydian')) .scale(cat('C minor','F dorian','G dorian','F# mixolydian'))
@ -501,7 +501,7 @@ stack(
.gain(.4) // turn down .gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff .cutoff(sine.slow(7).range(300,5000)) // automate cutoff
//.hush() //.hush()
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords ,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice .superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation .add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n" .n() // wrap in "n"
@ -531,7 +531,7 @@ samples({
perc: ['perc/002_perc2.wav'], perc: ['perc/002_perc2.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/'); }, 'github:tidalcycles/Dirt-Samples/master/');
"C^7 Am7 Dm7 G7".slow(2).voicings() "C^7 Am7 Dm7 G7".slow(2).voicings('lefthand')
.stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major')) .stack("0@6 [<1 2> <2 0> 1]@2".scale('C5 major'))
.n().slow(4) .n().slow(4)
.s('0040_FluidR3_GM_sf2_file') .s('0040_FluidR3_GM_sf2_file')
@ -626,7 +626,7 @@ stack(
s("mt lt ht").struct("x(3,8)").fast(2).gain(.5).room(.5).sometimes(x=>x.speed(".5")), 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), s("misc:2").speed(1).delay(.5).delaytime(1/3).gain(.4),
// chords // chords
note("[~ Gm7] ~ [~ Dm7] ~".voicings().superimpose(x=>x.add(.1))) note("[~ Gm7] ~ [~ Dm7] ~".voicings('lefthand').superimpose(x=>x.add(.1)))
.s('sawtooth').gain(.5) .s('sawtooth').gain(.5)
.cutoff(perlin.range(400,3000).slow(8)) .cutoff(perlin.range(400,3000).slow(8))
.decay(perlin.range(0.05,.2)).sustain(0) .decay(perlin.range(0.05,.2)).sustain(0)
@ -806,7 +806,7 @@ export const loungeSponge = `
await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc') await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc')
stack( stack(
note("<C^7 A7 Dm7 Fm7>/2".voicings()) note("<C^7 A7 Dm7 Fm7>/2".voicings('lefthand'))
.cutoff(sine.range(500,2000).round().slow(16)) .cutoff(sine.range(500,2000).round().slow(16))
.euclidLegato(3,8).csound('FM1') .euclidLegato(3,8).csound('FM1')
, ,
@ -823,7 +823,7 @@ export const arpoon = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.
// "Arpoon" by Felix Roos // "Arpoon" by Felix Roos
await samples('github:tidalcycles/Dirt-Samples/master') await samples('github:tidalcycles/Dirt-Samples/master')
"<<Am7 C^7> C7 F^7 [Fm7 E7b9]>".voicings() "<<Am7 C^7> C7 F^7 [Fm7 E7b9]>".voicings('lefthand')
.arp("[0,3] 2 [1,3] 2".fast(3)).legato(2) .arp("[0,3] 2 [1,3] 2".fast(3)).legato(2)
.add(perlin.range(0,0.2)).sub("<0 -12>/8") .add(perlin.range(0,0.2)).sub("<0 -12>/8")
.note().cutoff(perlin.range(500,4000)).resonance(12) .note().cutoff(perlin.range(500,4000)).resonance(12)

View File

@ -35,7 +35,7 @@ s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
.s('sawtooth') // waveform .s('sawtooth') // waveform
.gain(.4) // turn down .gain(.4) // turn down
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff .cutoff(sine.slow(7).range(300,5000)) // automate cutoff
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords ,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice .superimpose(x=>x.add(.04)) // add second, slightly detuned voice
.add(perlin.range(0,.5)) // random pitch variation .add(perlin.range(0,.5)) // random pitch variation
.n() // wrap in "n" .n() // wrap in "n"
@ -755,7 +755,7 @@ Transposes notes inside the scale by the number of steps:
Turns chord symbols into voicings, using the smoothest voice leading possible: Turns chord symbols into voicings, using the smoothest voice leading possible:
<MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()`} /> <MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()`} />
<!-- TODO: use voicing collection as first param + patternify. --> <!-- TODO: use voicing collection as first param + patternify. -->
@ -790,7 +790,7 @@ Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (L
If no outputName is given, it uses the first midi output it finds. If no outputName is given, it uses the first midi output it finds.
<MiniRepl <MiniRepl
tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>") tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>")
.midi()`} .midi()`}
/> />