diff --git a/packages/tonal/index.mjs b/packages/tonal/index.mjs index 54ac26b8..15eb9c4a 100644 --- a/packages/tonal/index.mjs +++ b/packages/tonal/index.mjs @@ -1,2 +1,5 @@ import './tonal.mjs'; import './voicings.mjs'; + +export * from './tonal.mjs'; +export * from './voicings.mjs'; diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a222b64b..f2188a74 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -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 }); +}); diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index b9a9a3a6..8332a087 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -4,31 +4,47 @@ Copyright (C) 2022 Strudel contributors - see . */ -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("".voicings(), "").note() + * stack("".voicings('lefthand'), "").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 + * "".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 }); +}); diff --git a/repl/src/testtunes.mjs b/repl/src/testtunes.mjs index fe5d8358..b92d4bd0 100644 --- a/repl/src/testtunes.mjs +++ b/repl/src/testtunes.mjs @@ -108,7 +108,7 @@ export const struct = `stack( export const magicSofa = `stack( " " .every(2, fast(2)) - .voicings(), + .voicings('lefthand'), " " ).transpose("<0 2 3 4>")`; // below doesn't work anymore due to constructor cleanup @@ -156,7 +156,7 @@ const synths = stack( scaleTranspose(8).early(3/8) ).apply(thru).tone(keys).mask("<~ x>/16"), "/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).apply(thru).tone(bass), - "/2".struct("~ [x@0.1 ~]".fast(2)).voicings().apply(thru).every(2, early(1/8)).tone(keys).mask("/8".early(1/4)) + "/2".struct("~ [x@0.1 ~]".fast(2)).voicings('lefthand').apply(thru).every(2, early(1/8)).tone(keys).mask("/8".early(1/4)) ) stack( 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) stack( - "".fast(2).struct("x ~ x@3 x ~ x ~ ~ ~ x ~ x@3".late(1/8)).early(1/8).slow(2).voicings(), + "".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) ).note().piano().slow(2)`; diff --git a/repl/src/tunes.mjs b/repl/src/tunes.mjs index 843be832..d604950c 100644 --- a/repl/src/tunes.mjs +++ b/repl/src/tunes.mjs @@ -138,7 +138,7 @@ const synths = stack( note("/2".apply(thru)) .struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)) .s('sawtooth').attack(0.001).decay(0.2).sustain(1).cutoff(500), - "/2".struct("~ [x@0.2 ~]".fast(2)).voicings() + "/2".struct("~ [x@0.2 ~]".fast(2)).voicings('lefthand') .apply(thru).every(2, early(1/8)).note().apply(keys).sustain(0) .delay(.4).delaytime(.12) .mask("/8".early(1/4)) @@ -245,7 +245,7 @@ export const festivalOfFingers = `// licensed with CC BY-NC-SA 4.0 https://creat // by Felix Roos const chords = ""; 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(4) .scale(cat('C minor','F dorian','G dorian','F# mixolydian')) @@ -501,7 +501,7 @@ stack( .gain(.4) // turn down .cutoff(sine.slow(7).range(300,5000)) // automate cutoff //.hush() - ,">".voicings() // chords + ,">".voicings('lefthand') // chords .superimpose(x=>x.add(.04)) // add second, slightly detuned voice .add(perlin.range(0,.5)) // random pitch variation .n() // wrap in "n" @@ -531,7 +531,7 @@ samples({ perc: ['perc/002_perc2.wav'], }, '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')) .n().slow(4) .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("misc:2").speed(1).delay(.5).delaytime(1/3).gain(.4), // chords - note("[~ Gm7] ~ [~ Dm7] ~".voicings().superimpose(x=>x.add(.1))) + note("[~ Gm7] ~ [~ Dm7] ~".voicings('lefthand').superimpose(x=>x.add(.1))) .s('sawtooth').gain(.5) .cutoff(perlin.range(400,3000).slow(8)) .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') stack( - note("/2".voicings()) + note("/2".voicings('lefthand')) .cutoff(sine.range(500,2000).round().slow(16)) .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 await samples('github:tidalcycles/Dirt-Samples/master') -"< C7 F^7 [Fm7 E7b9]>".voicings() +"< C7 F^7 [Fm7 E7b9]>".voicings('lefthand') .arp("[0,3] 2 [1,3] 2".fast(3)).legato(2) .add(perlin.range(0,0.2)).sub("<0 -12>/8") .note().cutoff(perlin.range(500,4000)).resonance(12) diff --git a/tutorial/tutorial.mdx b/tutorial/tutorial.mdx index 76bc31e1..fd3b245d 100644 --- a/tutorial/tutorial.mdx +++ b/tutorial/tutorial.mdx @@ -35,7 +35,7 @@ s("bd,[~ ],hh(3,4)") // drums .s('sawtooth') // waveform .gain(.4) // turn down .cutoff(sine.slow(7).range(300,5000)) // automate cutoff -,">".voicings() // chords +,">".voicings('lefthand') // chords .superimpose(x=>x.add(.04)) // add second, slightly detuned voice .add(perlin.range(0,.5)) // random pitch variation .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: -".voicings(), "").note()`} /> +".voicings('lefthand'), "").note()`} /> @@ -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. ".voicings(), "") + tune={`stack("".voicings('lefthand'), "") .midi()`} />