diff --git a/docs/dist/App.js b/docs/dist/App.js index fb19d714..dc37a72b 100644 --- a/docs/dist/App.js +++ b/docs/dist/App.js @@ -48,11 +48,12 @@ function App() { const cycle = useCycle({ onEvent: useCallback((time, event) => { try { - if (typeof event.value === "string") { - if (!isNote(event.value)) { - throw new Error("not a note: " + event.value); + if (!event.value?.onTrigger) { + const note = event.value?.value || event.value; + if (!isNote(note)) { + throw new Error("not a note: " + note); } - defaultSynth.triggerAttackRelease(event.value, event.duration, time); + defaultSynth.triggerAttackRelease(note, event.duration, time); } else { const {onTrigger} = event.value; onTrigger(time, event); diff --git a/docs/dist/tonal.js b/docs/dist/tonal.js index 0542b3d9..b48bc80e 100644 --- a/docs/dist/tonal.js +++ b/docs/dist/tonal.js @@ -1,10 +1,77 @@ -import {Note, Interval} from "../_snowpack/pkg/@tonaljs/tonal.js"; +import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js"; import {Pattern as _Pattern} from "../_snowpack/link/strudel.js"; const Pattern = _Pattern; +function toNoteEvent(event) { + if (typeof event === "string") { + return {value: event}; + } + if (event.value) { + return event; + } + throw new Error("not a valid note event: " + JSON.stringify(event)); +} +const mod = (n, m) => n < 0 ? mod(n + m, m) : n % m; +export function intervalDirection(from, to, direction = 1) { + const sign = Math.sign(direction); + const interval = sign < 0 ? Interval.distance(to, from) : Interval.distance(from, to); + return (sign < 0 ? "-" : "") + interval; +} +function scaleTranspose(scale, offset, note) { + let [tonic, scaleName] = Scale.tokenize(scale); + const {notes} = Scale.get(`${tonic} ${scaleName}`); + offset = Number(offset); + if (isNaN(offset)) { + throw new Error(`scale offset "${offset}" not a number`); + } + const {pc: fromPc, oct = 3} = Note.get(note); + const noteIndex = notes.indexOf(fromPc); + if (noteIndex === -1) { + throw new Error(`note "${note}" is not in scale "${scale}"`); + } + let i = noteIndex, o = oct, n = fromPc; + const direction = Math.sign(offset); + while (Math.abs(i - noteIndex) < Math.abs(offset)) { + i += direction; + const index = mod(i, notes.length); + if (direction < 0 && n === "C") { + o += direction; + } + n = notes[index]; + if (direction > 0 && n === "C") { + o += direction; + } + } + return n + o; +} +Pattern.prototype._mapNotes = function(func) { + return this.fmap((event) => { + const noteEvent = toNoteEvent(event); + return func(noteEvent); + }); +}; Pattern.prototype._transpose = function(intervalOrSemitones) { - const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); - return this.fmap((note) => Note.transpose(note, interval)); + return this._mapNotes(({value, scale}) => { + const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); + return {value: Note.transpose(value, interval), scale}; + }); }; Pattern.prototype.transpose = function(intervalOrSemitones) { return this._patternify(Pattern.prototype._transpose)(intervalOrSemitones); }; +Pattern.prototype._scaleTranspose = function(offset) { + return this._mapNotes(({value, scale}) => { + if (!scale) { + throw new Error("can only use scaleOffset after .scale"); + } + return {value: scaleTranspose(scale, Number(offset), value), scale}; + }); +}; +Pattern.prototype.scaleTranspose = function(offset) { + return this._patternify(Pattern.prototype._scaleTranspose)(offset); +}; +Pattern.prototype._scale = function(scale) { + return this._mapNotes((value) => ({...value, scale})); +}; +Pattern.prototype.scale = function(scale) { + return this._patternify(Pattern.prototype._scale)(scale); +}; diff --git a/docs/dist/tunes.js b/docs/dist/tunes.js index 8bce50c5..66f68f73 100644 --- a/docs/dist/tunes.js +++ b/docs/dist/tunes.js @@ -254,4 +254,17 @@ export const giantSteps = `stack( '[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]' ) ).slow(20);`; +export const transposedChords = `stack( + m('c2 eb2 g2'), + m('Cm7').voicings(['g2','c4']).slow(2) +).transpose( + slowcat(1, 2, 3, 2).slow(2) +).transpose(5)`; +export const scaleTranspose = `stack(f2, f3, c4, ab4) +.scale(sequence('F minor', 'F harmonic minor').slow(4)) +.scaleTranspose(sequence(0, -1, -2, -3).slow(4)) +.transpose(sequence(0, 1).slow(16)) +.synth('sawtooth') +.filter(800) +.gain(0.5)`; export default swimming;