import { type Editor } from "../main"; import { freqToMidi, chord as parseChord, noteNameToMidi, resolvePitchBend, safeScale, } from "zifferjs"; import { SkipEvent } from "./SkipEvent"; import { SoundParams } from "./SoundEvent"; import { centsToSemitones, edoToSemitones, ratiosToSemitones } from "zifferjs/src/scale"; import { safeMod } from "zifferjs/src/utils"; export type EventOperation = (instance: T, ...args: any[]) => void; export interface AbstractEvent { [key: string]: any; } export class AbstractEvent { /** * The Event class is the base class for all events. It provides a set of * functions that can be used to transform an Event. The functions are used * to create a chain of transformations that are applied to the Event. */ seedValue: string | undefined = undefined; randomGen: Function = Math.random; app: Editor; values: { [key: string]: any } = {}; constructor(app: Editor) { this.app = app; if (this.app.api.currentSeed) { this.randomGen = this.app.api.randomGen; } } evenbar = (func: Function) => { /** * Returns a transformed Event if the current bar is even. * * @param func - The function to be applied to the Event * @returns The transformed Event */ if (this.app.clock.time_position.bar % 2 === 0) { return this.modify(func); } else { return this; } }; even = (func: Function) => { /** * Returns a transformed Event if the current beat is even. * @param func - The function to be applied to the Event * @returns The transformed Event * */ if (this.app.clock.time_position.beat % 2 === 0) { return this.modify(func); } else { return this; } }; odd = (func: Function) => { /** * Returns a transformed Event if the current beat is odd. * * @param func - The function to be applied to the Event * @returns The transformed Event */ if (this.app.clock.time_position.beat % 2 !== 0) { return this.modify(func); } else { return this; } }; odds = (probability: number, func: Function): AbstractEvent => { /** * Returns a transformed Event with a given probability. * * @param probability - The probability of the Event being transformed * @param func - The function to be applied to the Event */ if (this.randomGen() < probability) { return this.modify(func); } return this; }; // @ts-ignore never = (func: Function): AbstractEvent => { /** * Never return a transformed Event. * * @param func - The function to be applied to the Event * @remarks This function is here for user convenience. */ return this; }; almostNever = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.025. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.025, func); }; rarely = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.1. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.1, func); }; scarcely = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.25. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.25, func); }; sometimes = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.5. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.5, func); }; often = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.75. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.75, func); }; frequently = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.9. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.9, func); }; almostAlways = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 0.985. * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.odds(0.985, func); }; always = (func: Function): AbstractEvent => { /** * Returns a transformed Event with a probability of 1. * @param func - The function to be applied to the Event * @remarks This function is here for user convenience. */ return this.modify(func); }; modify = (func: Function): AbstractEvent => { /** * Returns a transformed Event. This function is used internally by the * other functions of this class. It is just a wrapper for the function * application. */ return func(this); }; seed = (value: string | number): AbstractEvent => { /** * This function is used to set the seed of the random number generator. * @param value - The seed value * @returns The Event */ this.seedValue = value.toString(); this.randomGen = this.app.api.localSeededRandom(this.seedValue); return this; }; clear = (): AbstractEvent => { /** * This function is used to clear the seed of the random number generator. * @returns The Event */ this.app.api.clearLocalSeed(this.seedValue); return this; }; apply = (func: Function): AbstractEvent => { /** * This function is used to apply a function to the Event. * Simple function applicator * * @param func - The function to be applied to the Event * @returns The transformed Event */ return this.modify(func).update(); }; mod = (value: number): AbstractEvent => { this.values.originalPitch = safeMod(this.values.originalPitch, value); return this.update(); } noteLength = ( value: number | number[], ...kwargs: number[] ): AbstractEvent => { /** * This function is used to set the note length of the Event. */ if (kwargs.length > 0) { value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; } if (Array.isArray(value)) { this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v * 4 * this.app.clock.ppqn), ); } else { this.values.dur = this.app.clock.convertPulseToSecond( value * 4 * this.app.clock.ppqn, ); } if(this.current) { value = Array.isArray(value) ? value[this.index%value.length] : value; this.current.duration = value; } return this; }; protected processSound = ( sound: string | string[] | SoundParams | SoundParams[], ): SoundParams => { if (Array.isArray(sound) && typeof sound[0] === "string") { const s: string[] = []; const n: number[] = []; sound.forEach((str) => { const parts = (str as string).split(":"); s.push(parts[0]); if (parts[1]) { n.push(parseInt(parts[1])); } }); return { s, n: n.length > 0 ? n : undefined, dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), }; } else if (typeof sound === "object") { const validatedObj: SoundParams = { dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), ...(sound as Partial), }; return validatedObj; } else { if (sound.includes(":")) { const vals = sound.split(":"); const s = vals[0]; const n = parseInt(vals[1]); return { s, n, dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), }; } else { return { s: sound, dur: 0.5 }; } } }; } export abstract class AudibleEvent extends AbstractEvent { constructor(app: Editor) { super(app); } pitch = (value: number | number[], ...kwargs: number[]): this => { /* * This function is used to set the pitch of the Event. * @param value - The pitch value * @returns The Event */ if (kwargs.length > 0) { value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; } this.values["pitch"] = value; this.values["originalPitch"] = value; this.defaultPitchKeyScale(); return this.update(); }; pc = this.pitch; octave = (value: number | number[], ...kwargs: number[]): this => { /* * This function is used to set the octave of the Event. * @param value - The octave value * @returns The Event */ if (kwargs.length > 0) { value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; } this.values["paramOctave"] = value; if ( this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale ) { return this.update(); } return this; }; key = (value: string | string[], ...kwargs: string[]): this => { /* * This function is used to set the key of the Event. * @param value - The key value * @returns The Event */ if (kwargs.length > 0) { value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; } this.values["key"] = value; if ( (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale ) { return this.update(); } return this; }; defaultPitchKeyScale() { if (!this.values.key) this.values.key = 60; if (!(this.values.pitch || this.values.pitch === 0)) this.values.pitch = 0; if (!this.values.parsedScale) this.values.parsedScale = safeScale("major"); } scale = ( value: string | number | (number | string)[], ...kwargs: (string | number)[] ): this => { /* * This function is used to set the scale of the Event. * @param value - The scale value * @returns The Event */ if (kwargs.length > 0) { value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; } if (typeof value === "string" || typeof value === "number") { this.values.parsedScale = safeScale(value) as number[]; } else if (Array.isArray(value)) { this.values.parsedScale = value.map((v) => safeScale(v)); } this.defaultPitchKeyScale(); return this.update(); }; semitones(values: number|number[], ...rest: number[]) { const scaleValues = typeof values === "number" ? [values, ...rest] : values; this.values.parsedScale = safeScale(scaleValues); this.defaultPitchKeyScale(); return this.update(); } steps = this.semitones; cents(values: number|number[], ...rest: number[]) { const scaleValues = typeof values === "number" ? [values, ...rest] : values; this.values.parsedScale = safeScale(centsToSemitones(scaleValues)); this.defaultPitchKeyScale(); return this.update(); } ratios(values: number|number[], ...rest: number[]) { const scaleValues = typeof values === "number" ? [values, ...rest] : values; this.values.parsedScale = safeScale(ratiosToSemitones(scaleValues)); this.defaultPitchKeyScale(); return this.update(); } edo(value: number, intervals: string|number[] = new Array(value).fill(1)) { this.values.parsedScale = edoToSemitones(value, intervals); this.defaultPitchKeyScale(); return this.update(); } protected updateValue(key: string, value: T | T[] | null): this { if (value == null) return this; this.values[key] = value; return this; } public note = ( value: number | string | null, ...kwargs: number[] | string[] ) => { if (typeof value === "string") { const parsedNote = noteNameToMidi(value); return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity)); } else if (typeof value == null || value == undefined) { return new SkipEvent(); } else { return this.updateValue("note", [value, ...kwargs].flat(Infinity)); } }; public chord = (value: number | string, ...kwargs: number[]) => { if (typeof value === "string") { const chord = parseChord(value); return this.updateValue("note", chord); } else { const chord = [value, ...kwargs].flat(Infinity); return this.updateValue("note", chord); } }; public invert = (howMany: number = 0) => { if(howMany === 0) return this; if (this.values.note) { let notes = [...this.values.note]; notes = howMany < 0 ? [...notes].reverse() : notes; for (let i = 0; i < Math.abs(howMany); i++) { notes[i % notes.length] += howMany <= 0 ? -12 : 12; } return this.updateValue("note", notes); } else { return this; } }; public log = (key: string|string[], ...args: string[]) => { /* * Log values from values using log() * * @param key - The key(s) to log * @returns this and logs the values */ if (typeof key === "string") { if(args && args.length > 0) { this.app.api.log([key, ...args].map((k) => this.values[k])); } else { this.app.api.log(this.values[key]); } } else { this.app.api.log([...key, ...args].map((k) => this.values[k])); } return this; } public draw = (lambda: Function) => { lambda(this.values, (this.app.interface.drawings as HTMLCanvasElement).getContext("2d")); return this; } public clear = () => { this.app.api.clear(); return this; } freq = (value: number | number[], ...kwargs: number[]): this => { /* * This function is used to set the frequency of the Event. * @param value - The frequency value * @returns The Event */ if (kwargs.length > 0) { value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; } this.values["freq"] = value; if (Array.isArray(value)) { this.values["note"] = []; this.values["bend"] = []; for (const v of value) { const midiNote = freqToMidi(v); if (midiNote % 1 !== 0) { this.values["note"].push(Math.floor(midiNote)); this.values["bend"].push(resolvePitchBend(midiNote)[1]); } else { this.values["note"].push(midiNote); } } if (this.values.bend.length === 0) delete this.values.bend; } else { const midiNote = freqToMidi(value); if (midiNote % 1 !== 0) { this.values["note"] = Math.floor(midiNote); this.values["bend"] = resolvePitchBend(midiNote)[1]; } else { this.values["note"] = midiNote; } } return this; }; update = (): this => { // Overwrite in subclasses return this; }; cue = (functionName: string|Function): this => { this.app.api.cue(functionName); return this; } }