UNPKG

zifferjs

Version:

Zifferjs - Typescript variant of Ziffers

643 lines (563 loc) 24 kB
import { parse as parseZiffers } from './parser/ziffersParser.ts'; import { parse as parseScala } from './parser/scalaParser.ts'; import { DEFAULT_OPTIONS, isScale, getScale } from './defaults.ts'; import { centsToSemitones, edoToCents, ratiosToSemitones, voiceLead } from './scale.ts'; import { Base, Pitch, Chord, Roman, Rest, Event, SoundEvent, Options, NodeOptions, GlobalOptions, globalOptionKeys, ChangingOptions, Subdivision, Arpeggio, List } from './types.ts'; import { deepClone, seededRandom, filterObject } from './utils.ts'; import { rsystem } from './rules.ts'; import { explorativeTransform, TonnetzSpaces, hexaCycles, octaCycles, enneaCycles, cubeDance, powerTowers, boretzRegions, weitzmannRegions, octaTower } from 'ts-tonnetz'; type ZEvent = Pitch|Chord|Roman|Rest|SoundEvent; export class Ziffers { input: string; generator?: Generator<number>; generatorDone?: boolean = false; values: Base[]; evaluated: ZEvent[]; options: Options; counter: number = 0; redo: number = 1; index: number = -1; cycleIndex: number = 0; globalOptions : GlobalOptions; duration: number; scaleApplied: boolean = false; constructor(input: string, options: NodeOptions = {}, globalOptions: GlobalOptions = {}) { this.input = input; // Merge options with default options. TODO: Ignore some common options like degrees? options = {...DEFAULT_OPTIONS, ...options}; this.globalOptions = globalOptions; // Parse scala format if scale is not a scale name if(options.scale) { if(typeof options.scale === 'string') { if(!isScale(options.scale)) { options.scale = parseScala(options.scale) as number[]; } else { options.scaleName = options.scale; options.scale = getScale(options.scale) as number[]; } } options.parsedScale = options.scale as number[]; delete options.scale; } if(options.redo !== undefined) { this.redo = options.redo; } if(options && options.seed) { options.randomSeed = options.seed; options.seededRandom = seededRandom(options.seed); } // Check if globalOpions is empty if(Object.keys(globalOptions).length === 0) { this.globalOptions = getGlobalOption(options); } this.options = {nodeOptions: options}; try { this.values = parseZiffers(input, this.options); this.evaluated = this.evaluate(this.values); this.applyTransformations(); this.duration = this.totalDuration(); } catch (ex: any) { console.log(ex); // Handle parsing error // [...] this.values = []; this.evaluated = []; this.duration = 0; } } static fromGenerator(generator: Generator<number>, options: NodeOptions = {}): Ziffers { const number = generator.next().value; const ziff = this.fromNumber(number, options); ziff.generator = generator; return ziff; } static fromNumber(num: number, options: NodeOptions = {}) { const input = this.inputFromNumber(num); return new Ziffers(input, options); } static inputFromNumber(num: number) { let input = num.toString(); if(input.length>1) input = input.split("").join(" "); return input; } revaluate() { this.values = parseZiffers(this.input, this.options); this.evaluated = this.evaluate(this.values); this.applyTransformations(); this.duration = this.totalDuration(); } pitches(): (number|undefined|number[])[] { return this.evaluated.map((item: ZEvent) => { return item.collect("pitch"); }) } originalPitches(): (number|undefined|number[])[] { return this.evaluated.map((item: ZEvent) => { return item.collect("originalPitch"); }) } notes(): (number|undefined|number[])[] { return this.evaluated.map((item: ZEvent) => { return item.collect("note"); }); } collect(param: string) { return this.evaluated.map((item: ZEvent) => { return item.collect(param as keyof ZEvent); }); } sounds(): string[] { return this.evaluated.map((item: ZEvent) => { return item.collect("sound"); }); } indices(): number[] { return this.evaluated.map((item: ZEvent) => { return item.collect("soundIndex"); }); } freqs(): (number|undefined|number[])[] { return this.evaluated.map((item: ZEvent) => { return item.collect("freq"); }); } durations(): (number|undefined|number[])[] { return this.evaluated.map((item: ZEvent) => { return item.collect("duration"); }); } octaves(): (undefined|number|number[])[] { return this.evaluated.map((item: ZEvent) => { return item.collect("octave"); }); } retrograde(): Ziffers { this.evaluated = this.evaluated.reverse(); return this; } rotate(amount: number = 1): Ziffers { this.evaluated = [...this.evaluated.slice(amount), ...this.evaluated.slice(0, amount)]; return this; } every(amount: number = 1): Ziffers { this.evaluated = this.evaluated.filter((_, index) => { return index % amount === 0; }); return this; } scale(scale: string|number[]) { this.applyOptions({scale: scale}); this.scaleApplied = true; return this; } semitones = this.scale; cents(cents: number[]) { const scale = centsToSemitones(cents); this.applyOptions({scale: scale}); this.scaleApplied = true; return this; } ratios(ratios: number[]) { const scale = ratiosToSemitones(ratios); this.applyOptions({scale: scale}); this.scaleApplied = true; return this; } edo(edo: number, intervals: string|number[] = [1,1,1,1,1,1,1,1,1,1,1]) { const scaleInCents = edoToCents(edo, intervals); const semitones = centsToSemitones(scaleInCents); this.applyOptions({scale: semitones}); this.scaleApplied = true; return this; } key(key: string) { this.applyOptions({key: key}); return this; } octave(octave: number) { this.applyOptions({octave: octave}); return this; } invert(invert: number) { this.applyOptions({inversion: invert}); return this; } isInOptions(key: string, value: string|number) { return this.options.nodeOptions && this.options.nodeOptions[key as keyof NodeOptions] === value; } atLast(): boolean { return this.index+1 >= this.evaluated.length * (this.redo || 1); } clone(): Ziffers { return deepClone(this); } notStarted() { return this.index < 0 } peek() { return this.evaluated[this.index-1 || 0]; } hasStarted(): boolean { return this.index >= 0; } reset() { this.index = -1; this.counter = 0; } setRedo(amount: number) { // Set amount of repetitions for the current pattern. If amount is 0, it wil repeat the same pattern without re-evaluating. this.redo = amount; } next(): Event { // Check for the first run if(this.index<0) this.index = 0; // Get next item const nextEvent = this.evaluated[this.index % this.evaluated.length]; this.index++; this.counter++; // Check if next item is last if(this.redo > 0 && this.index >= this.evaluated.length * this.redo) { this.index = 0; this.cycleIndex++; if(this.generator) { const next = this.generator.next(); if(next.done) { this.generatorDone = true; } else { this.input = Ziffers.inputFromNumber(next.value); this.revaluate(); } } this.evaluated = this.evaluate(this.values); } return nextEvent; } applyOptions(options: ChangingOptions = {}) { this.evaluated = this.evaluate(this.evaluated, options); this.applyTransformations(); } applyTransformations() { // TODO: Make more generic if(this.globalOptions?.retrograde) { this.evaluated = this.evaluated.reverse(); } } update() { this.evaluated = this.evaluate(this.values); this.applyTransformations(); return this; } evaluate(values: Base[], options: ChangingOptions = {}): ZEvent[] { let items = values.map((node: Base) => { return node.evaluate(options); }).flat(Infinity).filter((node) => node !== undefined) as (ZEvent|Subdivision)[]; if(options.subdivisions) { items = resolveSubdivisions(items); } return items as ZEvent[]; } totalDuration(): number { const length = this.evaluated.reduce((acc: number, item: ZEvent) => { return acc + item.collect("duration"); }, 0); return length; } lead(): Ziffers { // Get first chord from evaluated events let lastChordIndex = this.evaluated.findIndex((o)=>{return o instanceof Chord}); if(lastChordIndex>=0) { for(let i = lastChordIndex+1; i<=this.evaluated.length; i++) { if(this.evaluated[i] instanceof Chord) { const aChord = (this.evaluated[lastChordIndex] as Chord); const bChord = (this.evaluated[i] as Chord); const leadedChord = voiceLead(aChord.notes(),bChord.notes()); const newChord = deepClone(bChord); newChord.voiceLeadFromNotes(leadedChord, this.options.nodeOptions!); this.evaluated[i] = newChord; lastChordIndex = i; } } } return this; } arpeggio(indexes: string|List|number[]|number): Ziffers { if(typeof indexes === "number") indexes = [indexes]; const filteredOptions = filterObject(this.options.nodeOptions,["octave", "scaleName", "key"],{scaleName: "scale"}); const options = {...DEFAULT_OPTIONS, ...filteredOptions}; const arpeggiated = this.evaluated.map((item: ZEvent) => { if(item instanceof Chord) { if(typeof indexes === "string") { indexes = new List({items: parseZiffers(indexes, this.options)} as object); } return new Arpeggio({chord: item, indexes: indexes} as object).evaluate(options); } return item; }); this.evaluated = arpeggiated.flat(Infinity) as ZEvent[]; return this; } toString(): string { return this.evaluated.map((item: ZEvent) => { return item.toString(); }).join(" "); } rules(rules: Record<string|number, string | number | ((...args: any[]) => string|number)>, gen: number = 1): Ziffers { const replacedString = rsystem(this.input, rules, gen); return new Ziffers(replacedString, this.options.nodeOptions!, this.globalOptions); } tonnetzChords(chordType: string, tonnetz: TonnetzSpaces = [3,4,5]): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { return item.tonnetzChord(chordType, tonnetz); } else return item; }); } return this; } tonnetz(transformation: string, tonnetz: TonnetzSpaces = [3,4,5]): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Chord) { if(item.notes().length===3) { return item.triadTonnetz(transformation, tonnetz, explorativeTransform); } else if(item.notes().length===4) { return item.tetraTonnetz(transformation, tonnetz, explorativeTransform); } } else return item; }).flat(Infinity) as ZEvent[]; } return this; } triadTonnetz(transformation: string, tonnetz: TonnetzSpaces = [3,4,5]): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Chord) { return item.triadTonnetz(transformation, tonnetz); } else return item; }).flat(Infinity) as ZEvent[]; } return this; } tetraTonnetz(transformation: string, tonnetz: TonnetzSpaces = [3,4,5]): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Chord) { return item.tetraTonnetz(transformation, tonnetz); } else return item; }).flat(Infinity) as ZEvent[]; } return this; } hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = hexaCycles(item.pitch as number, tonnetz, repeats, components); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } return item; }).flat(Infinity) as ZEvent[]; } return this; } octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4, components: number = 1): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = octaCycles(item.pitch as number, tonnetz, repeats, components); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } return item; }).flat(Infinity) as ZEvent[]; } return this; } enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = enneaCycles(item.pitch as number, tonnetz, repeats, components); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } return item; }).flat(Infinity) as ZEvent[]; } return this; } cubeDance(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = cubeDance(item.pitch as number, tonnetz, repeats); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } }).flat(Infinity) as ZEvent[]; } return this; } powerTowers(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = powerTowers(item.pitch as number, tonnetz, repeats); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } }).flat(Infinity) as ZEvent[]; } return this; } octaTower(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = octaTower(item.pitch as number, tonnetz, repeats, components); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } }).flat(Infinity) as ZEvent[]; } return this; } boretzRegions(tonnetz: TonnetzSpaces = [3, 4, 5]): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = boretzRegions(item.pitch as number, tonnetz); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } }).flat(Infinity) as ZEvent[]; } return this; } weitzmannRegions(tonnetz: TonnetzSpaces = [3, 4, 5]): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.map((item: ZEvent) => { if(item instanceof Pitch) { const chordCycle = weitzmannRegions(item.pitch as number, tonnetz); const zCycle = chordCycle.map((chord: number[]) => { return Chord.fromPitchClassArray(chord, (item.key || "C4"), (this.scaleApplied ? (item.scaleName || "CHROMATIC") : "CHROMATIC")).evaluate({duration: item.duration, octave: item.octave}); }); return zCycle as ZEvent[]; } }).flat(Infinity) as ZEvent[]; } return this; } shuffle(): Ziffers { if(this.evaluated) { for(let i = 0; i < this.evaluated.length; i++) { const j = Math.floor(Math.random() * (i + 1)); [this.evaluated[i], this.evaluated[j]] = [this.evaluated[j], this.evaluated[i]]; } } return this; } deal(amount: number): Ziffers { if(this.evaluated) { for(let i = 0; i < this.evaluated.length; i++) { const j = Math.floor(Math.random() * (i + 1)); [this.evaluated[i], this.evaluated[j]] = [this.evaluated[j], this.evaluated[i]]; } this.evaluated = this.evaluated.slice(0, amount); } return this; } from(index: number): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.slice(index); } return this; } to(index: number): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.slice(0, index); } return this; } between(start: number, end: number): Ziffers { if(this.evaluated) { this.evaluated = this.evaluated.slice(start, end); } return this; } at(index: number, ...rest: number[]): Ziffers { if(this.evaluated) { const indices = [index, ...rest]; this.evaluated = this.evaluated.filter((_, i) => indices.includes(i%this.evaluated.length)); } return this; } } const resolveSubdivisions = (values: (Chord|Rest|Pitch|SoundEvent|Subdivision)[], duration: number|undefined = undefined): ZEvent[] => { const sub = values.map((item: Chord|Rest|Pitch|SoundEvent|Subdivision) => { if(item instanceof Subdivision) { const length = item.evaluated.length const newDuration = (duration || item.duration) / length; return resolveSubdivisions(item.evaluated, newDuration); } else { item.duration = duration || item.duration; return item; } }); return sub.flat(Infinity) as ZEvent[]; } const getGlobalOption = (options: NodeOptions): GlobalOptions => { let globalOptions: GlobalOptions = {}; globalOptionKeys.forEach((key: string) => { if(options[key as keyof GlobalOptions] !== undefined) { const val = options[key as keyof GlobalOptions]; globalOptions[key as keyof GlobalOptions] = val; delete options[key as keyof GlobalOptions]; } }); return globalOptions; } export const pattern = (input: string, options: object = {}): Ziffers => { return new Ziffers(input, options); } // TODO: REMOVE? export function* cycle(generaterFn: (...kwargs: any[]) => Generator<number>, ...kwargs: any[]): Generator<number> { let generator = generaterFn(...kwargs); while (true) { const result = generator.next(); if (result.done) { generator = generaterFn(...kwargs); } else { yield result.value; } } } export function collect(count: number, generatorFn: (...kwargs: any[]) => Generator<number>, ...kwargs: any[]): number[] { const collectedValues: number[] = []; let generator = generatorFn(...kwargs); for (let i = 0; i < count; i++) { const result = generator.next(); if (result.done) { generator = generatorFn(...kwargs); } else { collectedValues.push(result.value); } } return collectedValues; }