diff --git a/src/API.ts b/src/API.ts index c6c4008..62f2cb4 100644 --- a/src/API.ts +++ b/src/API.ts @@ -9,7 +9,7 @@ import { tryEvaluate, evaluateOnce } from "./Evaluator"; import { DrunkWalk } from "./Utils/Drunk"; import { Editor } from "./main"; import { SoundEvent } from "./classes/SoundEvent"; -import { MidiEvent } from "./classes/MidiEvent"; +import { MidiEvent, MidiParams } from "./classes/MidiEvent"; import { LRUCache } from "lru-cache"; import { InputOptions, Player } from "./classes/ZPlayer"; import { @@ -28,6 +28,7 @@ import { import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; +import { SkipEvent } from './classes/SkipEvent'; interface ControlChange { channel: number; @@ -390,9 +391,10 @@ export class UserAPI { }; public midi = ( - value: number | object = 60, - velocity?: number, - channel?: number + value: number | number[] = 60, + velocity?: number | number[], + channel?: number | number[], + port?: number | string | number[] | string[] ): MidiEvent => { /** * Sends a MIDI note to the current MIDI output. @@ -402,24 +404,9 @@ export class UserAPI { * { channel: 0, velocity: 100, duration: 0.5 } */ - if (velocity !== undefined) { - // Check if value is of type number - if (typeof value === "number") { - value = { note: value }; - } - // @ts-ignore - value["velocity"] = velocity; - } + const event = {note: value, velocity, channel, port} as MidiParams - if (channel !== undefined) { - if (typeof value === "number") { - value = { note: value }; - } - // @ts-ignore - value["channel"] = channel; - } - - return new MidiEvent(value, this.app); + return new MidiEvent(event, this.app); }; public sysex = (data: Array): void => { @@ -1893,8 +1880,9 @@ export class UserAPI { // Trivial functions // ============================================================= - sound = (sound: string | object) => { - return new SoundEvent(sound, this.app); + sound = (sound: string | string[] | null | undefined) => { + if(sound) return new SoundEvent(sound, this.app); + else return new SkipEvent(); }; snd = this.sound; diff --git a/src/Utils/Generic.ts b/src/Utils/Generic.ts new file mode 100644 index 0000000..d6e91e6 --- /dev/null +++ b/src/Utils/Generic.ts @@ -0,0 +1,71 @@ +/* + * Transforms object with arrays into array of objects + * + * @param {Record} input - Object with arrays + * @param {string[]} ignoredKeys - Keys to ignore + * @returns {Record[]} Array of objects + * + */ +export function objectWithArraysToArrayOfObjects(input: Record, ignoredKeys: string[]): Record[] { + ignoredKeys = ignoredKeys.map((k) => Array.isArray(input[k]) ? undefined : k).filter((k) => k !== undefined) as string[]; + const keys = Object.keys(input).filter((k) => !ignoredKeys.includes(k)); + const maxLength = Math.max( + ...keys.map((k) => + Array.isArray(input[k]) ? (input[k] as any[]).length : 1 + ) + ); + + const output: Record[] = []; + + for (let i = 0; i < maxLength; i++) { + const event: Record = {}; + for (const k of keys) { + if (ignoredKeys.includes(k)) { + event[k] = input[k]; + } else { + if (Array.isArray(input[k])) { + event[k] = (input[k] as any[])[i % (input[k] as any[]).length]; + } else { + event[k] = input[k]; + } + } + } + output.push(event); + } + return output; + }; + +/* + * Transforms array of objects into object with arrays + * + * @param {Record[]} array - Array of objects + * @param {Record} mergeObject - Object that is merged to each object in the array + * @returns {object} Merged object with arrays + * + */ +export function arrayOfObjectsToObjectWithArrays>(array: T[], mergeObject: Record = {}): Record { + return array.reduce((acc, obj) => { + Object.keys(mergeObject).forEach((key) => { + obj[key as keyof T] = mergeObject[key]; + }); + Object.keys(obj).forEach((key) => { + if (!acc[key as keyof T]) { + acc[key as keyof T] = []; + } + (acc[key as keyof T] as unknown[]).push(obj[key]); + }); + return acc; + }, {} as Record); + } + + /* + * Filter certain keys from object + * + * @param {Record} obj - Object to filter + * @param {string[]} filter - Keys to filter + * @returns {object} Filtered object + * + */ + export function filterObject(obj: Record, filter: string[]): Record { + return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key))); + } \ No newline at end of file diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts index 6eab352..f486ed8 100644 --- a/src/classes/AbstractEvents.ts +++ b/src/classes/AbstractEvents.ts @@ -2,9 +2,7 @@ import { type Editor } from "../main"; import { freqToMidi, resolvePitchBend, - getScale, - isScale, - parseScala, + safeScale } from "zifferjs"; export abstract class Event { @@ -204,8 +202,20 @@ export abstract class Event { return this.modify(func); }; - length = (value: number): Event => { - this.values["length"] = value; + noteLength = (value: number | number[], ...kwargs: number[]): Event => { + /** + * 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["noteLength"] = value; + this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v*4*this.app.clock.ppqn)); + } else { + this.values["noteLength"] = value; + this.values.dur = this.app.clock.convertPulseToSecond(value*4*this.app.clock.ppqn); + } return this; }; } @@ -215,37 +225,101 @@ export abstract class AudibleEvent extends Event { super(app); } - octave = (value: number): this => { - this.values["octave"] = value; - this.update(); - return this; - }; - - key = (value: string): this => { - this.values["key"] = value; - this.update(); - return this; - }; - - scale = (value: string): this => { - if (!isScale(value)) { - this.values.parsedScale = parseScala(value) as number[]; - } else { - this.values.scaleName = value; - this.values.parsedScale = getScale(value) as number[]; + 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.update(); + this.values["pitch"] = value; + if(this.values.key && this.values.parsedScale) this.update(); + return this; + } + + 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["octave"] = value; + if(this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update(); return this; }; - freq = (value: number): 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) this.update(); + return this; + }; + + 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)); + } + if(this.values.key && (this.values.pitch || this.values.pitch === 0)) { + this.update(); + } + 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; - const midiNote = freqToMidi(value); - if (midiNote % 1 !== 0) { - this.values["note"] = Math.floor(midiNote); - this.values["bend"] = resolvePitchBend(midiNote)[1]; + 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 { - this.values["note"] = midiNote; + 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; }; diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index 1294d57..5fd0c5b 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -1,7 +1,8 @@ import { AudibleEvent } from "./AbstractEvents"; import { type Editor } from "../main"; import { MidiConnection } from "../IO/MidiConnection"; -import { midiToFreq, noteFromPc } from "zifferjs"; +import { noteFromPc, chord as parseChord } from "zifferjs"; +import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic"; export type MidiParams = { note: number; @@ -9,40 +10,49 @@ export type MidiParams = { channel?: number; port?: number; sustain?: number; + velocity?: number; } export class MidiEvent extends AudibleEvent { midiConnection: MidiConnection; - constructor(input: number | object, public app: Editor) { + constructor(input: MidiParams, public app: Editor) { super(app); - if (typeof input === "number") this.values["note"] = input; - else this.values = input; + this.values = input; this.midiConnection = app.api.MidiConnection; } - chord = (value: MidiParams[]): this => { - this.values["chord"] = value; - return this; + public chord = (value: string) => { + this.values.note = parseChord(value); + return this; }; - note = (value: number): this => { + note = (value: number | number[]): this => { this.values["note"] = value; return this; }; - sustain = (value: number): this => { + sustain = (value: number | number[]): this => { this.values["sustain"] = value; return this; }; - channel = (value: number): this => { + velocity = (value: number | number[]): this => { + this.values["velocity"] = value; + return this; + } + + channel = (value: number | number[]): this => { this.values["channel"] = value; return this; }; - port = (value: number | string): this => { - this.values["port"] = this.midiConnection.getMidiOutputIndex(value); + port = (value: number | string | number[] | string[]): this => { + if(typeof value === "string"){ + this.values["port"] = this.midiConnection.getMidiOutputIndex(value); + } else if(Array.isArray(value)){ + this.values["port"] = value.map((v) => typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v); + } return this; }; @@ -75,37 +85,46 @@ export class MidiEvent extends AudibleEvent { }; update = (): void => { - const [note, bend] = noteFromPc( - this.values.key || "C4", - this.values.pitch || 0, - this.values.parsedScale || "MAJOR", - this.values.octave || 0 - ); - this.values.note = note; - this.values.freq = midiToFreq(note); - if (bend) this.values.bend = bend; + // Get key, pitch, parsedScale and octave from this.values object + const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]); + + const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]); + + events.forEach((event) => { + const [note, bend] = noteFromPc( + event.key as number || "C4", + event.pitch as number || 0, + event.parsedScale as number[] || event.scale || "MAJOR", + event.octave as number || 0 + ); + event.note = note; + if(bend) event.bend = bend; + }); + + const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams; + + this.values.note = newArrays.note; + if(newArrays.bend) this.values.bend = newArrays.bend; }; out = (): void => { - function play(event: MidiEvent, params?: MidiParams): void { - const paramChannel = params && params.channel ? params.channel : 0; - const channel = event.values.channel ? event.values.channel : paramChannel; - const velocity = event.values.velocity ? event.values.velocity : 100; - const paramNote = params && params.note ? params.note : 60; - const note = event.values.note ? event.values.note : paramNote; + function play(event: MidiEvent, params: MidiParams): void { + const channel = params.channel ? params.channel : 0; + const velocity = params.velocity ? params.velocity : 100; + const note = params.note ? params.note : 60; - const sustain = event.values.sustain - ? event.values.sustain * + const sustain = params.sustain + ? params.sustain * event.app.clock.pulse_duration * event.app.api.ppqn() : event.app.clock.pulse_duration * event.app.api.ppqn(); - const bend = event.values.bend ? event.values.bend : undefined; - - const port = event.values.port - ? event.midiConnection.getMidiOutputIndex(event.values.port) - : event.midiConnection.getCurrentMidiPortIndex(); + const bend = params.bend ? params.bend : undefined; + const port = params.port + ? event.midiConnection.getMidiOutputIndex(params.port) + : event.midiConnection.getCurrentMidiPortIndex() || 0; + event.midiConnection.sendMidiNote( note, channel, @@ -116,13 +135,11 @@ export class MidiEvent extends AudibleEvent { ); } - if(this.values.chord) { - this.values.chord.forEach((p: MidiParams) => { - play(this, p); - }); - } else { - play(this); - } + const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[]; + + events.forEach((p: MidiParams) => { + play(this,p); + }); }; } diff --git a/src/classes/RestEvent.ts b/src/classes/RestEvent.ts index 4f4ec53..862d2d5 100644 --- a/src/classes/RestEvent.ts +++ b/src/classes/RestEvent.ts @@ -4,11 +4,11 @@ import { Event } from "./AbstractEvents"; export class RestEvent extends Event { constructor(length: number, app: Editor) { super(app); - this.values["length"] = length; + this.values["noteLength"] = length; } _fallbackMethod = (): Event => { - return RestEvent.createRestProxy(this.values["length"], this.app); + return RestEvent.createRestProxy(this.values["noteLength"], this.app); }; public static createRestProxy = ( diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 80ac9f8..4c753d3 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -1,5 +1,6 @@ import { type Editor } from "../main"; import { AudibleEvent } from "./AbstractEvents"; +import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic"; import { chord as parseChord, midiToFreq, @@ -13,328 +14,375 @@ import { } from "superdough"; export type SoundParams = { - dur: number; - s?: string; + dur: number | number[]; + s?: undefined | string | string[]; + n?: undefined | number | number[]; + analyze?: boolean; + note?: number | number[]; + freq?: number | number[]; + pitch?: number | number[]; + key?: string; + scale?: string; + parsedScale?: number[]; + octave?: number | number[]; }; export class SoundEvent extends AudibleEvent { nudge: number; + sound: any; - constructor(sound: string | object, public app: Editor) { + private methodMap = { + volume: ["volume", "vol"], + zrand: ["zrand", "zr"], + curve: ["curve"], + slide: ["slide", "sld"], + deltaSlide: ["deltaSlide", "dslide"], + pitchJump: ["pitchJump", "pj"], + pitchJumpTime: ["pitchJumpTime", "pjt"], + lfo: ["lfo"], + znoise: ["znoise"], + noise: ["noise"], + zmod: ["zmod"], + zcrush: ["zcrush"], + zdelay: ["zdelay"], + sustainVolume: ["sustainVolume"], + tremolo: ["tremolo"], + dur: ["dur"], + zzfx: ["zzfx"], + fmi: ["fmi"], + fmh: ["fmh"], + fmenv: ["fmenv"], + fmattack: ["fmattack", "fmatk"], + fmdecay: ["fmdecay", "fmdec"], + fmsustain: ["fmsustain", "fmsus"], + fmrelease: ["fmrelease", "fmrel"], + fmvelocity: ["fmvelocity", "fmvel"], + fmwave: ["fmwave", "fmw"], + fmadsr: (a: number, d: number, s: number, r: number) => { + this.updateValue("fmattack", a); + this.updateValue("fmdecay", d); + this.updateValue("fmsustain", s); + this.updateValue("fmrelease", r); + return this; + }, + fmad: (a: number, d: number) => { + this.updateValue("fmattack", a); + this.updateValue("fmdecay", d); + return this; + }, + ftype: ["ftype"], + fanchor: ["fanchor"], + attack: ["attack", "atk"], + decay: ["decay", "dec"], + sustain: ["sustain", "sus"], + release: ["release", "rel"], + adsr: (a: number, d: number, s: number, r: number) => { + this.updateValue("attack", a); + this.updateValue("decay", d); + this.updateValue("sustain", s); + this.updateValue("release", r); + return this; + }, + ad: (a: number, d: number) => { + this.updateValue("attack", a); + this.updateValue("decay", d); + this.updateValue("sustain", 0.0); + this.updateValue("release", 0.0); + return this; + }, + lpenv: ["lpenv", "lpe"], + lpattack: ["lpattack", "lpa"], + lpdecay: ["lpdecay", "lpd"], + lpsustain: ["lpsustain", "lps"], + lprelease: ["lprelease", "lpr"], + cutoff: (value: number, resonance?: number) => { + this.updateValue("cutoff", value); + if (resonance) { + this.updateValue("resonance", resonance); + } + return this; + }, + lpf: (value: number, resonance?: number) => { + this.updateValue("cutoff", value); + if (resonance) { + this.updateValue("resonance", resonance); + } + return this; + }, + resonance: (value: number) => { + if (value >= 0 && value <= 1) { + this.updateValue("resonance", 50 * value); + } + return this; + }, + lpadsr: (depth: number, a: number, d: number, s: number, r: number) => { + this.updateValue("lpenv", depth); + this.updateValue("lpattack", a); + this.updateValue("lpdecay", d); + this.updateValue("lpsustain", s); + this.updateValue("lprelease", r); + return this; + }, + lpad: (depth: number, a: number, d: number) => { + this.updateValue("lpenv", depth); + this.updateValue("lpattack", a); + this.updateValue("lpdecay", d); + this.updateValue("lpsustain", 0); + this.updateValue("lprelease", 0); + return this; + }, + hpenv: ["hpenv", "hpe"], + hpattack: ["hpattack", "hpa"], + hpdecay: ["hpdecay", "hpd"], + hpsustain: ["hpsustain", "hpsus"], + hprelease: ["hprelease", "hpr"], + hcutoff: (value: number, resonance?: number) => { + this.updateValue("hcutoff", value); + if (resonance) { + this.updateValue("hresonance", resonance); + } + return this; + }, + hpq: (value: number) => { + this.updateValue("hresonance", value); + return this; + }, + hpadsr: (depth: number, a: number, d: number, s: number, r: number) => { + this.updateValue("hpenv", depth); + this.updateValue("hpattack", a); + this.updateValue("hpdecay", d); + this.updateValue("hpsustain", s); + this.updateValue("hprelease", r); + return this; + }, + hpad: (depth: number, a: number, d: number) => { + this.updateValue("hpenv", depth); + this.updateValue("hpattack", a); + this.updateValue("hpdecay", d); + this.updateValue("hpsustain", 0); + this.updateValue("hprelease", 0); + return this; + }, + bpenv: ["bpenv", "bpe"], + bpattack: ["bpattack", "bpa"], + bpdecay: ["bpdecay", "bpd"], + bpsustain: ["bpsustain", "bps"], + bprelease: ["bprelease", "bpr"], + bandf: (value: number, resonance?: number) => { + this.updateValue("bandf", value); + if (resonance) { + this.updateValue("bandq", resonance); + } + return this; + }, + bpf: (value: number, resonance?: number) => { + this.updateValue("bandf", value); + if (resonance) { + this.updateValue("bandq", resonance); + } + return this; + }, + bandq: ["bandq", "bpq"], + bpadsr: (depth: number, a: number, d: number, s: number, r: number) => { + this.updateValue("bpenv", depth); + this.updateValue("bpattack", a); + this.updateValue("bpdecay", d); + this.updateValue("bpsustain", s); + this.updateValue("bprelease", r); + return this; + }, + bpad: (depth: number, a: number, d: number) => { + this.updateValue("bpenv", depth); + this.updateValue("bpattack", a); + this.updateValue("bpdecay", d); + this.updateValue("bpsustain", 0); + this.updateValue("bprelease", 0); + return this; + }, + vib: ["vib"], + vibmod: ["vibmod"], + fm: (value: number | string) => { + if (typeof value === "number") { + this.values["fmi"] = value; + } else { + let values = value.split(":"); + this.values["fmi"] = parseFloat(values[0]); + if (values.length > 1) this.values["fmh"] = parseFloat(values[1]); + } + return this; + }, + loop: ["loop"], + loopBegin: ["loopBegin", "loopb"], + loopEnd: ["loopEnd", "loope"], + begin: ["begin"], + end: ["end"], + gain: ["gain"], + dbgain: (value: number) => { + this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + return this; + }, + db: (value: number) => { + this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + return this; + }, + velocity: ["velocity", "vel"], + pan: ["pan"], + cut: ["cut"], + clip: ["clip"], + n: ["n"], + speed: ["speed", "spd"], + coarse: ["coarse"], + crush: ["crush"], + shape: ["shape"], + vowel: ["vowel", "vow"], + delay: ["delay", "del"], + delayfeedback: ["delayfeedback", "delayfb"], + delaytime: ["delaytime", "delayt"], + orbit: ["orbit", "o"], + room: ["room", "rm"], + roomfade: ["roomfade", "rfade"], + roomlp: ["roomlp", "rlp"], + roomdim: ["roomdim", "rdim"], + sound: ["sound", "s"], + size: (value: number) => { + this.updateValue("roomsize", value); + return this; + }, + sz: (value: number) => { + this.updateValue("roomsize", value); + return this; + }, + comp: ["compressor", "cmp"], + ratio: (value: number) => { + this.updateValue("compressorRatio", value); + return this; + }, + knee: (value: number) => { + this.updateValue("compressorKnee", value); + return this; + }, + compAttack: (value: number) => { + this.updateValue("compressorAttack", value); + return this; + }, + compRelease: (value: number) => { + this.updateValue("compressorRelease", value); + return this; + }, + stretch: (beat: number) => { + this.updateValue("unit", "c"); + this.updateValue("speed", 1 / beat); + this.updateValue("cut", beat); + return this; + }, + }; + + + constructor(sound: string | string[] | SoundParams, public app: Editor) { super(app); this.nudge = app.dough_nudge / 100; - if (typeof sound === "string") { + + for (const [methodName, keys] of Object.entries(this.methodMap)) { + if (Symbol.iterator in Object(keys)) { + for (const key of keys as string[]) { + // @ts-ignore + this[key] = (value: number) => this.updateValue(keys[0], value); + } + } else { + // @ts-ignore + this[methodName] = keys; + } + } + this.values = this.processSound(sound); + } + + private 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), + analyze: true + }; + } else if (typeof sound === 'object') { + const validatedObj: SoundParams = { + dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), + analyze: true, + ...sound as Partial + }; + return validatedObj; + } else { if (sound.includes(":")) { - this.values = { - s: sound.split(":")[0], - n: sound.split(":")[1], - dur: app.clock.convertPulseToSecond(app.clock.ppqn), - analyze: true, + 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), + analyze: true }; } else { - this.values = { s: sound, dur: 0.5, analyze: true }; + return { s: sound, dur: 0.5, analyze: true }; } - } else { - this.values = sound; } } - private updateValue(key: string, value: T): this { + private updateValue(key: string, value: T | T[] | SoundParams[] | null): this { + if (value == null) return this; this.values[key] = value; return this; } // ================================================================================ - // ZZFX Sound Parameters + // AbstactEvent overrides // ================================================================================ - public volume = (value: number) => this.updateValue("volume", value); - public vol = this.volume; - public zrand = (value: number) => this.updateValue("zrand", value); - public curve = (value: number) => this.updateValue("curve", value); - public slide = (value: number) => this.updateValue("slide", value); - public sld = this.slide; - public deltaSlide = (value: number) => this.updateValue("deltaSlide", value); - public dslide = this.deltaSlide; - public pitchJump = (value: number) => this.updateValue("pitchJump", value); - public pj = this.pitchJump; - public pitchJumpTime = (value: number) => - this.updateValue("pitchJumpTime", value); - public pjt = this.pitchJumpTime; - public lfo = (value: number) => this.updateValue("lfo", value); - public znoise = (value: number) => this.updateValue("znoise", value); - public noise = (value: number) => this.updateValue("noise", value); - public zmod = (value: number) => this.updateValue("zmod", value); - public zcrush = (value: number) => this.updateValue("zcrush", value); - public zdelay = (value: number) => this.updateValue("zdelay", value); - public sustainVolume = (value: number) => - this.updateValue("sustainVolume", value); - public tremolo = (value: number) => this.updateValue("tremolo", value); - public dur = (value: number) => this.updateValue("dur", value); - public zzfx = (value: number[]) => this.updateValue("zzfx", value); - - // ================================================================================ - // Basic Audio Engine Parameters - // ================================================================================ - - // FM Synthesis - public fmi = (value: number) => this.updateValue("fmi", value); - public fmh = (value: number) => this.updateValue("fmh", value); - public fmenv = (value: "lin" | "exp") => this.updateValue("fmenv", value); - public fmattack = (value: number) => this.updateValue("fmattack", value); - public fmatk = this.fmattack; - public fmdecay = (value: number) => this.updateValue("fmdecay", value); - public fmdec = this.fmdecay; - public fmsustain = (value: number) => this.updateValue("fmsustain", value); - public fmsus = this.fmsustain; - public fmrelease = (value: number) => this.updateValue("fmrelease", value); - public fmrel = this.fmrelease; - public fmvelocity = (value: number) => this.updateValue("fmvelocity", value); - public fmvel = this.fmvelocity; - public fmwave = (value: "sine" | "triangle" | "sawtooth" | "square") => - this.updateValue("fmwave", value); - public fmw = this.fmwave; - - // Filter type - public ftype = (value: "12db" | "24db") => this.updateValue("ftype", value); - public fanchor = (value: number) => this.updateValue("fanchor", value); - - // Amplitude Envelope - public attack = (value: number) => this.updateValue("attack", value); - public atk = this.attack; - public decay = (value: number) => this.updateValue("decay", value); - public dec = this.decay; - public sustain = (value: number) => this.updateValue("sustain", value); - public sus = this.sustain; - public release = (value: number) => this.updateValue("release", value); - public rel = this.release; - public adsr = (a: number, d: number, s: number, r: number) => { - this.attack(a); - this.decay(d); - this.sustain(s); - this.release(r); - return this; - }; - public ad = (a: number, d: number) => { - this.attack(a); - this.decay(d); - this.sustain(0.0); - this.release(0.0); - return this; - }; - - // Lowpass filter - public lpenv = (value: number) => this.updateValue("lpenv", value); - public lpe = (value: number) => this.updateValue("lpenv", value); - public lpattack = (value: number) => this.updateValue("lpattack", value); - public lpa = this.lpattack; - public lpdecay = (value: number) => this.updateValue("lpdecay", value); - public lpd = this.lpdecay; - public lpsustain = (value: number) => this.updateValue("lpsustain", value); - public lps = this.lpsustain; - public lprelease = (value: number) => this.updateValue("lprelease", value); - public lpr = this.lprelease; - public cutoff = (value: number, resonance?: number) => { - this.updateValue("cutoff", value); - if (resonance) { - this.resonance(resonance) + modify = (func: Function): this => { + const funcResult = func(this); + if (funcResult instanceof Object) return funcResult; + else { + func(this.values); + this.update(); + return this; } - return this; - } - public lpf = this.cutoff; - public resonance = (value: number) => { - if (value >= 0 && value <= 1) { - this.updateValue( - "resonance", - 50 * value + }; + + update = (): void => { + const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]); + const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]); + + events.forEach((event) => { + const [note, _] = noteFromPc( + event.key as number || "C4", + event.pitch as number || 0, + event.parsedScale as number[] || event.scale || "MAJOR", + event.octave as number || 0 ); - } - return this; - } - public lpq = this.resonance; - public lpadsr = ( - depth: number, - a: number, - d: number, - s: number, - r: number - ) => { - this.lpenv(depth); - this.lpattack(a); - this.lpdecay(d); - this.lpsustain(s); - this.lprelease(r); - return this; - }; - public lpad = ( - depth: number, - a: number, - d: number, - ) => { - this.lpenv(depth); - this.lpattack(a); - this.lpdecay(d); - this.lpsustain(0); - this.lprelease(0); - return this; + event.note = note; + event.freq = midiToFreq(note); + }); + + const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams; + + this.values.note = newArrays.note; + this.values.freq = newArrays.freq; }; - - // Highpass filter - - public hpenv = (value: number) => this.updateValue("hpenv", value); - public hpe = (value: number) => this.updateValue("hpe", value); - public hpattack = (value: number) => this.updateValue("hpattack", value); - public hpa = this.hpattack; - public hpdecay = (value: number) => this.updateValue("hpdecay", value); - public hpd = this.hpdecay; - public hpsustain = (value: number) => this.updateValue("hpsustain", value); - public hpsus = this.hpsustain; - public hprelease = (value: number) => this.updateValue("hprelease", value); - public hpr = this.hprelease; - public hcutoff = (value: number) => this.updateValue("hcutoff", value); - public hpf = this.hcutoff; - public hresonance = (value: number, resonance?: number) => { - this.updateValue("hresonance", value); - if (resonance) { - this.resonance(resonance) - } - return this; - } - public hpq = this.hresonance; - public hpadsr = ( - depth: number, - a: number, - d: number, - s: number, - r: number - ) => { - this.hpenv(depth); - this.hpattack(a); - this.hpdecay(d); - this.hpsustain(s); - this.hprelease(r); - return this; - }; - public hpad = ( - depth: number, - a: number, - d: number, - ) => { - this.hpenv(depth); - this.hpattack(a); - this.hpdecay(d); - this.hpsustain(0); - this.hprelease(0); - return this; - }; - - // Bandpass filter - - public bpenv = (value: number) => this.updateValue("bpenv", value); - public bpe = (value: number) => this.updateValue("bpe", value); - public bpattack = (value: number) => this.updateValue("bpattack", value); - public bpa = this.bpattack; - public bpdecay = (value: number) => this.updateValue("bpdecay", value); - public bpd = this.bpdecay; - public bpsustain = (value: number) => this.updateValue("bpsustain", value); - public bps = this.bpsustain; - public bprelease = (value: number) => this.updateValue("bprelease", value); - public bpr = this.bprelease; - public bandf = (value: number, resonance?: number) => { - this.updateValue("bandf", value); - if (resonance) { - this.resonance(resonance) - } - return this; - } - public bpf = this.bandf; - public bandq = (value: number) => this.updateValue("bandq", value); - public bpq = this.bandq; - public bpadsr = ( - depth: number, - a: number, - d: number, - s: number, - r: number - ) => { - this.bpenv(depth); - this.bpattack(a); - this.bpdecay(d); - this.bpsustain(s); - this.bprelease(r); - return this; - }; - public bpad = ( - depth: number, - a: number, - d: number, - ) => { - this.bpenv(depth); - this.bpattack(a); - this.bpdecay(d); - this.bpsustain(0); - this.bprelease(0); - return this; - }; - - - public freq = (value: number) => this.updateValue("freq", value); - public f = this.freq; - public vib = (value: number) => this.updateValue("vib", value); - public vibmod = (value: number) => this.updateValue("vibmod", value); - public fm = (value: number | string) => { - if (typeof value === "number") { - this.values["fmi"] = value; - } else { - let values = value.split(":"); - this.values["fmi"] = parseFloat(values[0]); - if (values.length > 1) this.values["fmh"] = parseFloat(values[1]); - } - return this; - }; - - // Sampler looping - public loop = (value: number) => this.updateValue("loop", value); - public loopBegin = (value: number) => this.updateValue("loopBegin", value); - public loopEnd = (value: number) => this.updateValue("loopEnd", value); - public begin = (value: number) => this.updateValue("begin", value); - public end = (value: number) => this.updateValue("end", value); - - // Gain management - public gain = (value: number) => this.updateValue("gain", value); - public dbgain = (value: number) => - this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); - public db = this.dbgain; - public velocity = (value: number) => this.updateValue("velocity", value); - public vel = this.velocity; - - // Panoramic control (stereo) - public pan = (value: number) => this.updateValue("pan", value); - - // Frequency management - - public sound = (value: string) => this.updateValue("s", value); - public chord = ( - value: string | object[] | number[] | number, - ...kwargs: number[] - ) => { - if (typeof value === "string") { + public chord = (value: string) => { const chord = parseChord(value); - value = chord.map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - } else if (value instanceof Array && typeof value[0] === "number") { - value = (value as number[]).map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - } else if (typeof value === "number" && kwargs.length > 0) { - value = [value, ...kwargs].map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - } - return this.updateValue("chord", value); + return this.updateValue("note", chord); }; + public invert = (howMany: number = 0) => { if (this.values.chord) { let notes = this.values.chord.map( @@ -352,10 +400,6 @@ export class SoundEvent extends AudibleEvent { return this; } }; - public snd = this.sound; - public cut = (value: number) => this.updateValue("cut", value); - public clip = (value: number) => this.updateValue("clip", value); - public n = (value: number) => this.updateValue("n", value); public note = (value: number | string | null) => { if (typeof value === "string") { return this.updateValue("note", noteNameToMidi(value)); @@ -365,108 +409,15 @@ export class SoundEvent extends AudibleEvent { return this.updateValue("note", value); } }; - public speed = (value: number) => this.updateValue("speed", value); - public spd = this.speed; - - // Creative sampler effects - public coarse = (value: number) => this.updateValue("coarse", value); - public crush = (value: number) => this.updateValue("crush", value); - public shape = (value: number) => this.updateValue("shape", value); - public vowel = (value: number) => this.updateValue("vowel", value); - public vow = this.vowel; - - // Delay control - public delay = (value: number) => this.updateValue("delay", value); - public del = this.delay; - public delayfeedback = (value: number) => - this.updateValue("delayfeedback", value); - public delayfb = this.delayfeedback; - public delaytime = (value: number) => this.updateValue("delaytime", value); - public delayt = this.delaytime; - - // Orbit management - public orbit = (value: number) => this.updateValue("orbit", value); - public o = this.orbit; - - // Reverb management - public room = (value: number) => this.updateValue("room", value); - public rm = this.room; - public roomfade = (value: number) => this.updateValue("roomfade", value); - public rfade = this.roomfade; - public roomlp = (value: number) => this.updateValue("roomlp", value); - public rlp = this.roomlp; - public roomdim = (value: number) => this.updateValue("roomdim", value); - public rdim = this.roomdim; - public size = (value: number) => this.updateValue("roomsize", value); - public sz = this.size; - public rev = (room: number, size: number, fade?: number, lp?: number, dim?: number) => { - this.updateValue("room", room) - this.updateValue("roomsize", size) - if (fade) - this.updateValue("roomfade", fade) - if (lp) - this.updateValue("roomlp", lp) - if (dim) - this.updateValue("roomdim", dim) - - return this; - } - - // Compressor - public comp = (value: number) => this.updateValue("compressor", value); - public cmp = this.comp; - public ratio = (value: number) => this.updateValue("compressorRatio", value); - public rt = this.ratio; - public knee = (value: number) => this.updateValue("compressorKnee", value); - public kn = this.knee; - public compAttack = (value: number) => - this.updateValue("compressorAttack", value); - public cmpa = this.compAttack; - public compRelease = (value: number) => - this.updateValue("compressorRelease", value); - public cmpr = this.compRelease; - - // Unit - public stretch = (beat: number) => { - this.updateValue("unit", "c"); - this.updateValue("speed", 1 / beat); - this.updateValue("cut", beat); - return this; - }; - - // ================================================================================ - // AbstactEvent overrides - // ================================================================================ - - modify = (func: Function): this => { - const funcResult = func(this); - if (funcResult instanceof Object) return funcResult; - else { - func(this.values); - this.update(); - return this; - } - }; - - update = (): void => { - const [note, _] = noteFromPc( - this.values.key || "C4", - this.values.pitch || 0, - this.values.parsedScale || "MAJOR", - this.values.octave || 0 - ); - this.values.freq = midiToFreq(note); - }; out = (): void => { - if (this.values.chord) { - this.values.chord.forEach((obj: { [key: string]: number }) => { - const copy = { ...this.values }; - copy.freq = obj.freq; - superdough(copy, this.nudge, this.values.dur); - }); - } else { - superdough(this.values, this.nudge, this.values.dur); + const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]); + for (const event of events) { + superdough( + event, + this.nudge, + event.dur + ); } }; } diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index 85fb3ba..57a87fb 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -5,6 +5,7 @@ import { SkipEvent } from "./SkipEvent"; import { SoundEvent, SoundParams } from "./SoundEvent"; import { MidiEvent, MidiParams } from "./MidiEvent"; import { RestEvent } from "./RestEvent"; +import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic"; export type InputOptions = { [key: string]: string | number }; @@ -139,37 +140,39 @@ export class Player extends Event { }; sound(name?: string) { - if (this.areWeThereYet()) { const event = this.next() as Pitch | Chord | ZRest; const noteLengthInSeconds = this.app.clock.convertPulseToSecond(event.duration*4*this.app.clock.ppqn); if (event instanceof Pitch) { const obj = event.getExisting( "freq", + "note", "pitch", "key", "scale", "octave", "parsedScale" - ); + ) as SoundParams; if(event.sound) name = event.sound as string; - if(event.soundIndex) obj.n = event.soundIndex; + if(event.soundIndex) obj.n = event.soundIndex as number; obj.dur = noteLengthInSeconds; return new SoundEvent(obj, this.app).sound(name || "sine"); } else if (event instanceof Chord) { const pitches = event.pitches.map((p) => { return p.getExisting( "freq", + "note", "pitch", "key", "scale", "octave", "parsedScale" ); - }); - const sound: SoundParams = {dur: noteLengthInSeconds}; - if(name) sound.s = name; - return new SoundEvent(sound, this.app).chord(pitches); + }) as SoundParams[]; + const add = { dur: noteLengthInSeconds } as SoundParams; + if(name) add.s = name; + let sound = arrayOfObjectsToObjectWithArrays(pitches,add) as SoundParams; + return new SoundEvent(sound, this.app); } else if (event instanceof ZRest) { return RestEvent.createRestProxy(event.duration, this.app); } @@ -189,16 +192,17 @@ export class Player extends Event { "scale", "octave", "parsedScale", - ); + ) as MidiParams; if (event instanceof Pitch) { - if(event.soundIndex) obj.channel = event.soundIndex; + if(event.soundIndex) obj.channel = event.soundIndex as number; const note = new MidiEvent(obj, this.app); return value ? note.note(value) : note; } else if (event instanceof ZRest) { return RestEvent.createRestProxy(event.duration, this.app); } else if (event instanceof Chord) { const pitches = event.midiChord() as MidiParams[]; - return new MidiEvent(obj, this.app).chord(pitches); + const obj = arrayOfObjectsToObjectWithArrays(pitches) as MidiParams; + return new MidiEvent(obj, this.app); } } else { return SkipEvent.createSkipProxy();