UNPKG

zifferjs

Version:

Zifferjs - Typescript variant of Ziffers

799 lines (743 loc) 28.6 kB
import { noteFromPc, midiToFreq, scaleLength, safeScale, parseRoman, midiToPitchClass, namedChordFromDegree, noteNameToMidi, getScaleNotes, getChordFromScale } from './scale.ts'; import { OPERATORS, getScale, getRandomScale, DEFAULT_DURATION } from './defaults.ts'; import { deepClone, safeMod } from './utils.ts'; import { Tetrachord, TonnetzSpaces, TriadChord, chordFromTonnetz, seventhsTransform, transform } from 'ts-tonnetz'; export const globalOptionKeys: string[] = ["retrograde"]; export type GlobalOptions = { retrograde?: boolean; } export type Options = { nodeOptions?: NodeOptions; defaultDurs?: {[key: string]: number}; } export type NodeOptions = { key?: string|number; add?: number; scale?: string|number[]; parsedScale?: number[]|undefined; scaleName?: string; duration?: number; octave?: number; index?: number; port?: string; channel?: number; velocity?: number; degrees?: boolean; redo?: number; seed?: string; randomSeed?: string; seededRandom?: Function; globalOptions?: GlobalOptions; retrograde?: boolean; } export type ChangingOptions = { octave?: number; duration?: number; scale?: string|number[]; key?: string; subdivisions?: boolean; inversion?: number; sound?: string; soundIndex?: number|RandomPitch; } export type Node = NodeOptions & { // Common type: string; text: string; location: Location; // Pitch pitch?: number; originalPitch?: number; freq?: number; note?: number; bend?: number; // Sound sound?: string; soundIndex?: number; // Chord pitches?: Node[]; // List operation left?: Node[]; right?: Node[]; operation?: string; // Repeat times?: number; item?: Node; // Random node min?: number; max?: number; } type Location = { end: Position; start: Position; } type Position = { offset: number; line: number; column: number; } export abstract class Base { type!: string; text!: string; location!: Location; constructor(data: Partial<Node>) { this.type = this.constructor.name; Object.assign(this, data); } public clone(): any { return deepClone(this); } collect<K extends keyof Base>(name: K): Base[K] { return this[name]; } refresh(): void { // Overwrite in subclasses } // @ts-ignore evaluate(options: ChangingOptions = {}): Base|Base[]|undefined { return this; } // @ts-ignore prevaluate(options: ChangingOptions = {}): Base { return this; } evaluateValue(): any { return this.text; } toString(): string { return this.text; } } export abstract class Event extends Base { duration!: number; modifiedEvent: Event|undefined = undefined; globalOptions!: GlobalOptions; sound?: string|Base; soundIndex?: number|RandomPitch; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } collect(name: string): any { // Overwrite in subclasses // @ts-ignore return this[name]; } sometimesBy(probability: number, func: Function): Event { if(Math.random() < probability) { return this.modify(func); } return this; } sometimes(func: Function): Event { return this.sometimesBy(0.5, func); } rarely(func: Function): Event { return this.sometimesBy(0.1, func); } often(func: Function): Event { return this.sometimesBy(0.9, func); } update(func: Function): Event { func(this); this.refresh(); return this; } modify(func: Function): Event { this.modifiedEvent = this.clone(); func(this.modifiedEvent); this.modifiedEvent!.refresh(); return this.modifiedEvent!; } skip(): Event { return this } scale(_scale: string|number[]): Event { return this } randomScale(): Event { return this } retrograde(): Event { return this } asObject(): object { const attributes: Record<keyof this, any> = {} as Record<keyof this, any>; for (const key in this) { if (Object.prototype.hasOwnProperty.call(this, key)) { attributes[key as keyof this] = this[key as keyof this]; } } return attributes; } getExisting(...args: string[]): {[key: string]: any} { const existing = args.reduce((acc, value) => { if(Object.prototype.hasOwnProperty.call(this, value)) { const val = this[value as keyof this]; if(val || val===0) acc[value] = this[value as keyof this]; } return acc; }, {} as {[key: string]: any}); return existing; } mapExisting(fromKeys: string[], toKeys: string[]): {[key: string]: any} { const existing = fromKeys.reduce((acc, value, index) => { if(Object.prototype.hasOwnProperty.call(this, value)) { const val = this[value as keyof this]; if(val || val===0) acc[toKeys[index]] = this[value as keyof this]; } return acc; }, {} as {[key: string]: any}); return existing; } } export class Pitch extends Event { pitch!: number|RandomPitch; originalPitch?: number; add?: number; freq?: number; note?: number; octave?: number; pitchOctave?: number; addedOctave?: number; bend?: number; key?: string|number; parsedScale?: number[]; scaleName?: string; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } refresh(): void { this.evaluate(); } evaluate(options: ChangingOptions = {}): Pitch { const clone = deepClone(this); if(!clone.duration) { clone.duration = (options.duration || options.duration === 0) ? options.duration : DEFAULT_DURATION; } if(options.scale) { if(typeof options.scale === "string" && clone.scaleName !== options.scale) { clone.scaleName = options.scale; } if(clone.originalPitch) { clone.pitch = clone.originalPitch; clone.octave = 0; clone.pitchOctave = 0; } clone.parsedScale = safeScale(options.scale) as number[]; } if(options.key) clone.key = options.key; if(options.soundIndex || options.soundIndex===0) { if(!(typeof options.soundIndex === "number")) { clone.soundIndex = (options.soundIndex as Event).evaluateValue() as unknown as number; } else { clone.soundIndex = options.soundIndex; } } if(options.sound) { if(!(typeof options.sound === "string")) { clone.sound = (options.sound as Event).evaluateValue() as unknown as string; } else { clone.sound = options.sound; } } if(clone.pitch || clone.pitch === 0) { if(clone.pitch instanceof RandomPitch) { clone.pitch = clone.pitch.evaluateValue(); clone.originalPitch = clone.pitch; } else if(!clone.originalPitch) { clone.originalPitch = clone.pitch; } if(clone.parsedScale && clone.originalPitch >= clone.parsedScale.length) { clone.pitchOctave = Math.floor(clone.originalPitch / clone.parsedScale.length); clone.pitch = safeMod(clone.originalPitch, clone.parsedScale.length); } if(options.octave || clone.pitchOctave || clone.addedOctave) clone.octave = (options.octave || 0) + (clone.pitchOctave || 0) + (clone.addedOctave || 0); const [note,bend] = noteFromPc(clone.key!, (clone.pitch as number)!, clone.parsedScale!, clone.octave!); clone.note = clone.add ? note+clone.add : note; clone.freq = midiToFreq(clone.note); if(bend) { clone.bend = bend; } } if(clone.soundIndex instanceof RandomPitch) { clone.soundIndex = clone.soundIndex.evaluateValue(); } return clone; } prevaluate() { if(this.pitch instanceof RandomPitch) { this.pitch = this.pitch.evaluateValue(); this.originalPitch = this.pitch; } return this; } collect<K extends keyof Pitch>(name: K): Pitch[K] { return this[name]; } scale(name: string): Pitch { if(this.scaleName!==name) { this.scaleName = name; this.parsedScale = getScale(name) as number[]; return this.evaluate(); } return this; } randomScale(): Pitch { this.parsedScale = getRandomScale(); return this.evaluate(); } tonnetzChord(chordType: string, tonnetz: TonnetzSpaces = [3,4,5]): Chord { const chordNotes = chordFromTonnetz(this.note!, chordType, tonnetz); const pitches = chordNotes.map((note) => { const rootedNote = note + (typeof this.key == "number" ? note : noteNameToMidi(this.key!)) + ((this.octave||0)*12); const pitchClass = midiToPitchClass(rootedNote, this.key!, this.scaleName!); const pitch = new Pitch({ note: rootedNote, duration: this.duration, key: this.key, parsedScale: this.parsedScale, scaleName: this.scaleName, pitch: pitchClass.pc, originalPitch: pitchClass.pc, octave: (this.octave||0)+pitchClass.octave, add: pitchClass.add, text: pitchClass.text }); return pitch as unknown as Node; }); return new Chord({pitches: pitches, duration: this.duration}); } } export class Sound extends Pitch { constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluateValue() { return this.sound; } } export class SoundEvent extends Event { item!: Base constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options?: ChangingOptions): Event|Event[]|undefined { let soundValue = this.sound; if(options) { options.sound = soundValue as string; } else { options = {sound: soundValue as string} } const node: Event = this.item.evaluate(options) as Event; return node; } } export class SoundIndex extends Event { item!: Base; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options?: ChangingOptions): Event|Event[]|undefined { if(options) { options.soundIndex = this.soundIndex; } else { options = {soundIndex: this.soundIndex} } return this.item.evaluate(options) as Event; } } export class Chord extends Event { pitches!: Pitch[]; chordName?: string; inversion?: number; key?: number|string; scaleName?: string; parsedScale?: number[]; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); if(this.pitches && this.pitches.length > 0) { this.duration = Math.max(...this.pitches.map((pitch) => pitch.duration!)); } } static fromPitchClassArray(pcs: number[], key: string|number, scaleName: string): Chord { const pitches = pcs.map((pc) => { return new Pitch({originalPitch: pc, pitch: pc, key: key, scaleName: scaleName, parsedScale: safeScale(scaleName)}) as unknown as Node; }); return new Chord({pitches: pitches}); } evaluate(options: ChangingOptions = {}): Chord { const dupChord = deepClone(this); if(options.scale) { if(typeof options.scale === "string") dupChord.scaleName = options.scale; dupChord.parsedScale = safeScale(options.scale) as number[]; } if(options.inversion || dupChord.inversion) { dupChord.pitches = dupChord.invert((options.inversion || dupChord.inversion)!, options); } else { dupChord.pitches = dupChord.pitches.map((pitch) => pitch.evaluate(options)); } dupChord.duration = Math.max(...dupChord.pitches.map((pitch) => pitch.duration!)); return dupChord; } collect<K extends keyof Pitch>(name: K): Pitch[K] { const collect = this.pitches.map((pitch: Pitch) => pitch.collect(name)) as unknown as Pitch[K]; return collect; } notes(): number[] { return this.pitches.map((pitch) => pitch.note!) as number[]; } freqs(): number[] { return this.pitches.map((pitch) => pitch.freq!) as number[]; } pcs(): number[] { return this.pitches.map((pitch) => pitch.pitch!) as number[]; } midiChord(): {[key: string]: any} { const params = this.pitches.map((pitch) => pitch.mapExisting(["note","soundIndex"],["note","channel"])); return params; } scale(name: string): Chord { if(this.scaleName!==name) return this.evaluate({scale: name}) this.pitches.forEach((pitch) => pitch.scale(name)); return this; } invert(value: number, options: ChangingOptions = {}): Pitch[] { if(value===0) return this.pitches; const newPcs = value < 0 ? this.pitches.reverse() : this.pitches; for (let i = 0; i < Math.abs(value); i++) { const pc = newPcs[i % newPcs.length]; if (!pc.addedOctave) pc.addedOctave = 0; pc.addedOctave += (value <= 0 ? -1 : 1); //pc.octave = pc.pitchOctave; } const a = newPcs.map((pitch) => pitch.evaluate(options)); return a; } voiceLeadFromNotes(leadedNotes: number[], options: NodeOptions): void { this.pitches = this.pitches.map((p: Pitch, i: number) => { if(leadedNotes[i]) { const newPitch = midiToPitchClass(leadedNotes[i], options.key, options.scaleName); const pc = deepClone(p); pc.pitch = newPitch.pc; pc.octave = newPitch.octave; pc.add = newPitch.add; pc.text = newPitch.text; pc.note = leadedNotes[i]; pc.freq = midiToFreq(leadedNotes[i]); return pc; } else return deepClone(p); }); } triadTonnetz(transformationInput: string, tonnetz: TonnetzSpaces = [3,4,5], transformFunc: Function = transform): Chord|Chord[] { const notes = this.notes(); if(notes.length === 3) { const splittedTransforms = transformationInput.split(" "); const allTransforms = splittedTransforms.map((transformation) => { const transformedChord = (transformFunc(notes as TriadChord, transformation, tonnetz) as TriadChord)?.sort((a,b) => a-b); if(!transformedChord) return this; const parsedScale = this.pitches[0].parsedScale!; const chord = new Chord({pitches: transformedChord.map((pc) => { const newPC = midiToPitchClass(pc, this.key, this.scaleName); const newPitch = new Pitch({originalPitch: newPC.pc, pitch: newPC.pc, add: newPC.add, duration: this.duration, key: this.key, scaleName: this.scaleName, parsedScale: parsedScale}); return newPitch as unknown as Node; })}); return chord.evaluate(); }); return allTransforms; } else return this; } tetraTonnetz(transformationInput: string, tonnetz: TonnetzSpaces = [3,4,5], transformFunc: Function = seventhsTransform): Chord|Chord[] { const notes = this.notes(); if(notes.length === 4) { const splittedTransforms = transformationInput.split(" "); const allTransforms = splittedTransforms.map((transformation) => { const transformedChord = (transformFunc(notes as Tetrachord, transformation, tonnetz) as Tetrachord)?.sort((a,b) => a-b); if(!transformedChord) return this; const parsedScale = this.pitches[0].parsedScale!; const chord = new Chord({pitches: transformedChord.map((pc) => { const newPC = midiToPitchClass(pc, this.key, this.scaleName); const newPitch = new Pitch({originalPitch: newPC.pc, pitch: newPC.pc, add: newPC.add, duration: this.duration, key: this.key, scaleName: this.scaleName, parsedScale: parsedScale}); return newPitch as unknown as Node; })}); return chord.evaluate(); }); return allTransforms; } else return this; } } export class Roman extends Chord { roman!: string; romanNumeral!: number; octave?: number; chordOctave?: number; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): Roman { const dup = deepClone(this); if(options.scale && typeof options.scale === "string") { dup.scaleName = options.scale; } dup.romanNumeral = parseRoman(dup.roman); const key = dup.key || options.key || 60; const scale = dup.scaleName || "MAJOR"; const parsedScale = safeScale(scale) as number[]; let octave = (dup.chordOctave || 0) + (options.octave || 0); if(dup.chordName) { const chord = namedChordFromDegree(dup.romanNumeral, dup.chordName, key, scale, octave); const pitchObj = chord.map((note) => { return midiToPitchClass(note,key,scale); }); dup.pitches = pitchObj.map((pc) => { const pitchOct = octave+pc.octave; return new Pitch({originalPitch: pc.pc, pitch: pc.pc, octave: pitchOct, key: key, parsedScale: parsedScale, add: pc.add, duration: this.duration}).evaluate(options); }); } else { const scaleNotes: number[] = getScaleNotes(scale, 0, 7); const chrom_pcs: number[] = getChordFromScale(dup.romanNumeral, 0, scale); const pcs: number[] = chrom_pcs.map((note) => { return scaleNotes.indexOf(note); }); dup.pitches = pcs.map((pc) => { return new Pitch({originalPitch: pc, pitch: pc, octave: octave, key: key, parsedScale: parsedScale, duration: this.duration}).evaluate(options); }); } if(options.inversion || dup.inversion) { const inversion = options.inversion || dup.inversion; dup.pitches = dup.invert(inversion!, options); } dup.duration = Math.max(...dup.pitches.map((pitch) => pitch.duration!)); return dup; } } export class Rest extends Event { constructor(data: Partial<Node>) { super(data); } evaluate(options: ChangingOptions = {}): Rest { if(!this.duration) this.duration = (options.duration || options.duration === 0) ? options.duration : DEFAULT_DURATION; return this; } } export class RandomPitch extends Pitch { min!: number; max!: number; randomSeed?: string; seededRandom?: Function; random: Function; randomize: boolean = true; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); if(!data.min) this.min = 0; if(!data.max) this.max = scaleLength(this.parsedScale!); if(this.seededRandom) { this.random = this.seededRandom; } else { this.random = Math.random; } } evaluate(options: ChangingOptions = {}): Pitch { this.pitch = this.evaluateValue(); this.originalPitch = this.pitch; const pitch = new Pitch({pitch: this.pitch, originalPitch: this.pitch, text: this.pitch.toString()}).evaluate(options); return pitch; } evaluateValue(): number { return Math.floor(this.random() * (this.max - this.min + 1)) + this.min; } } export class OctaveChange extends Base { octave!: number; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}) { options.octave = this.octave + (options.octave || 0); return undefined; } } export class DurationChange extends Base { duration!: number; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}) { options.duration = this.duration; return undefined; } } export class Repeat extends Base { times!: number; item!: Base[]; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): Pitch[] { const repeated = [...Array(this.times)].map(() => this.item).flat(Infinity) as Base[]; return repeated.map((item) => { return item.evaluate(options) }) as Pitch[]; } } export class List extends Base { items!: Base[]; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): (Pitch|Chord)[] { return this.items.map((item: Base) => { return item.evaluate(options); }).flat(Infinity) as unknown as (Pitch|Chord)[]; } prevaluate(): List { this.items.forEach((item) => item.prevaluate()); return this; } } export class Arpeggio extends List { chord!: Chord|List; indexes!: List|number[]; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): (Pitch|Chord)[] { let chord = this.chord.evaluate(); if(!Array.isArray(chord)) { chord = [chord]; } return chord.map((chord) => { if(!(chord instanceof Chord)) return chord; const chordLength = chord.pitches.length; if(this.indexes instanceof List) { const pitchIndexes = this.indexes.evaluate(deepClone(options)).filter((item) => item !== undefined); return pitchIndexes.map((idx: Pitch|Chord) => { if(idx instanceof Chord) { const dupChord = idx.clone() as Chord; dupChord.pitches = dupChord.pitches.map((pc) => { return chord.pitches[pc.pitch as number % chordLength]; }); return dupChord.evaluate(); } else if (idx instanceof Pitch){ const origPitch = chord.pitches[idx.pitch as number % chordLength]; const dupPitch = idx.clone() as Pitch; dupPitch.pitch = origPitch.pitch; dupPitch.pitchOctave = origPitch.pitchOctave; dupPitch.add = (dupPitch.add||0)+(origPitch.add||0); dupPitch.key = origPitch.key; dupPitch.scaleName = origPitch.scaleName; dupPitch.parsedScale = origPitch.parsedScale; return dupPitch.evaluate(); } return idx; }) } else if(Array.isArray(this.indexes)) { const pitches = this.indexes.map(i => { if(Array.isArray(i)) { const chordPitches = i.map((index) => { return chord.pitches[index % chordLength]; }) as unknown as Node[]; return new Chord({pitches: chordPitches, duration: chord.duration}).evaluate() } else { const pitch = chord.pitches[i % chordLength]; return pitch.evaluate(); } }) as unknown as (Pitch|Chord)[]; return pitches; } else { return []; } }).flat(Infinity) as unknown as (Pitch|Chord)[]; } } export class Subdivision extends Base { duration!: number; items!: (Pitch|Chord|Rest|Subdivision)[]; evaluated!: (Pitch|Chord|Rest|Subdivision)[]; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): Subdivision { options.subdivisions = true; //const dup = deepClone(this); this.duration = options.duration || DEFAULT_DURATION; this.evaluated = this.items.map((item: Base) => { return item.evaluate(options); }).filter((v) => v).flat(Infinity) as unknown as Pitch[]; return this; } } export class RepeatList extends Base { times!: number; items!: Base[]; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): Pitch|Chord|Rest[] { const evaluated = this.items.map((item) => { return item.evaluate(options) }) as Pitch|Chord|Rest[]; const repeated = [...Array(this.times)].map(() => evaluated).flat(Infinity) as Pitch|Chord|Rest[]; return repeated; } } export class ListOperation extends Base { left!: List; right!: List; operation!: string; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); } evaluate(options: ChangingOptions = {}): Pitch[] { const left = this.left.evaluate(options).flat(Infinity); const right = this.right.evaluate(options).flat(Infinity); // Parse operator from string to javascript operator const operator = OPERATORS[this.operation]; // Create pairs of elements const pairs: [Pitch, Pitch][] = right.flatMap((r) => { return left.map((l) => { return [l.clone(), r.clone()] as [Pitch, Pitch]; }); }); // Do pairwise operations const result: Pitch[] = pairs.map((p: [Pitch, Pitch]) => { p[0].pitch = operator(p[0].originalPitch, p[1].originalPitch); p[0].originalPitch = p[0].pitch as number; return p[0].evaluate(options); }); return result; } } export class Cycle extends Event { items!: Base[]; index: number; constructor(data: Partial<Node>) { super(data); Object.assign(this, data); this.items = this.items.filter((item) => item !== undefined); this.index = 0; } nextItem(options: ChangingOptions = {}): Base | Base[] | undefined { let value = this.items[this.index%this.items.length] as Base|Base[]|undefined; while(value instanceof Cycle) { value = value.nextItem(options); } this.index = this.index+1; if(value instanceof Base) { const test = value.evaluate(options) as Event; return test; } return value; } evaluate(options: ChangingOptions = {}): Base | Base[] | undefined { const value = this.nextItem(options); return value; } evaluateValue(options: ChangingOptions = {}): any { const next = this.nextItem(options); if(next instanceof Base) { return next.evaluateValue(); } else { return next; } } }