Refactor object mangling and add midi support

This commit is contained in:
2023-11-03 16:07:39 +02:00
parent 269df3f899
commit a09e3a76be
7 changed files with 242 additions and 161 deletions

View File

@ -9,7 +9,7 @@ import { tryEvaluate, evaluateOnce } from "./Evaluator";
import { DrunkWalk } from "./Utils/Drunk"; import { DrunkWalk } from "./Utils/Drunk";
import { Editor } from "./main"; import { Editor } from "./main";
import { SoundEvent } from "./classes/SoundEvent"; import { SoundEvent } from "./classes/SoundEvent";
import { MidiEvent } from "./classes/MidiEvent"; import { MidiEvent, MidiParams } from "./classes/MidiEvent";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
import { InputOptions, Player } from "./classes/ZPlayer"; import { InputOptions, Player } from "./classes/ZPlayer";
import { import {
@ -390,9 +390,10 @@ export class UserAPI {
}; };
public midi = ( public midi = (
value: number | object = 60, value: number | number[] = 60,
velocity?: number, velocity?: number | number[],
channel?: number channel?: number | number[],
port?: number | string | number[] | string[]
): MidiEvent => { ): MidiEvent => {
/** /**
* Sends a MIDI note to the current MIDI output. * Sends a MIDI note to the current MIDI output.
@ -402,24 +403,9 @@ export class UserAPI {
* { channel: 0, velocity: 100, duration: 0.5 } * { channel: 0, velocity: 100, duration: 0.5 }
*/ */
if (velocity !== undefined) { const event = {note: value, velocity, channel, port} as MidiParams
// Check if value is of type number
if (typeof value === "number") {
value = { note: value };
}
// @ts-ignore
value["velocity"] = velocity;
}
if (channel !== undefined) { return new MidiEvent(event, this.app);
if (typeof value === "number") {
value = { note: value };
}
// @ts-ignore
value["channel"] = channel;
}
return new MidiEvent(value, this.app);
}; };
public sysex = (data: Array<number>): void => { public sysex = (data: Array<number>): void => {
@ -1893,7 +1879,7 @@ export class UserAPI {
// Trivial functions // Trivial functions
// ============================================================= // =============================================================
sound = (sound: string | object) => { sound = (sound: string | string[]) => {
return new SoundEvent(sound, this.app); return new SoundEvent(sound, this.app);
}; };

70
src/Utils/Generic.ts Normal file
View File

@ -0,0 +1,70 @@
/*
* Transforms object with arrays into array of objects
*
* @param {Record<string, any>} input - Object with arrays
* @param {string[]} ignoredKeys - Keys to ignore
* @returns {Record<string, any>[]} Array of objects
*
*/
export function objectWithArraysToArrayOfObjects(input: Record<string, any>, ignoredKeys: string[]): Record<string, any>[] {
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<string, any>[] = [];
for (let i = 0; i < maxLength; i++) {
const event: Record<string, any> = {};
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<string, any>[]} array - Array of objects
* @param {Record<string, any>} mergeObject - Object that is merged to each object in the array
* @returns {object} Merged object with arrays
*
*/
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(array: T[], mergeObject: Record<string, any> = {}): Record<string, any> {
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<keyof T, any[]>);
}
/*
* Filter certain keys from object
*
* @param {Record<string, any>} obj - Object to filter
* @param {string[]} filter - Keys to filter
* @returns {object} Filtered object
*
*/
export function filterObject(obj: Record<string, any>, filter: string[]): Record<string, any> {
return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key)));
}

View File

@ -204,8 +204,11 @@ export abstract class Event {
return this.modify(func); return this.modify(func);
}; };
length = (value: number): Event => { noteLength = (value: number): Event => {
this.values["length"] = value; /**
* This function is used to set the note length of the Event.
*/
this.values["noteLength"] = value;
return this; return this;
}; };
} }
@ -215,30 +218,63 @@ export abstract class AudibleEvent extends Event {
super(app); super(app);
} }
pitch = (value: number): this => {
/*
* This function is used to set the pitch of the Event.
* @param value - The pitch value
* @returns The Event
*/
this.values["pitch"] = value;
if(this.values.key && this.values.parsedScale) this.update();
return this;
}
pc = this.pitch;
octave = (value: number): this => { octave = (value: number): this => {
/*
* This function is used to set the octave of the Event.
* @param value - The octave value
* @returns The Event
*/
this.values["octave"] = value; this.values["octave"] = value;
this.update(); if(this.values.key && this.values.pitch && this.values.parsedScale) this.update();
return this; return this;
}; };
key = (value: string): this => { key = (value: string): this => {
/*
* This function is used to set the key of the Event.
* @param value - The key value
* @returns The Event
*/
this.values["key"] = value; this.values["key"] = value;
this.update(); if(this.values.pitch && this.values.parsedScale) this.update();
return this; return this;
}; };
scale = (value: string): this => { scale = (value: string): this => {
/*
* This function is used to set the scale of the Event.
* @param value - The scale value
* @returns The Event
*/
if (!isScale(value)) { if (!isScale(value)) {
this.values.parsedScale = parseScala(value) as number[]; this.values.parsedScale = parseScala(value) as number[];
} else { } else {
this.values.scaleName = value; this.values.scaleName = value;
this.values.parsedScale = getScale(value) as number[]; this.values.parsedScale = getScale(value) as number[];
} }
this.update(); if(this.values.key && this.values.pitch) this.update();
return this; return this;
}; };
freq = (value: number): this => { freq = (value: number): this => {
/*
* This function is used to set the frequency of the Event.
* @param value - The frequency value
* @returns The Event
*/
this.values["freq"] = value; this.values["freq"] = value;
const midiNote = freqToMidi(value); const midiNote = freqToMidi(value);
if (midiNote % 1 !== 0) { if (midiNote % 1 !== 0) {

View File

@ -1,7 +1,8 @@
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main"; import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection"; 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 = { export type MidiParams = {
note: number; note: number;
@ -9,40 +10,49 @@ export type MidiParams = {
channel?: number; channel?: number;
port?: number; port?: number;
sustain?: number; sustain?: number;
velocity?: number;
} }
export class MidiEvent extends AudibleEvent { export class MidiEvent extends AudibleEvent {
midiConnection: MidiConnection; midiConnection: MidiConnection;
constructor(input: number | object, public app: Editor) { constructor(input: MidiParams, public app: Editor) {
super(app); super(app);
if (typeof input === "number") this.values["note"] = input; this.values = input;
else this.values = input;
this.midiConnection = app.api.MidiConnection; this.midiConnection = app.api.MidiConnection;
} }
chord = (value: MidiParams[]): this => { public chord = (value: string) => {
this.values["chord"] = value; this.values.note = parseChord(value);
return this; return this;
}; };
note = (value: number): this => { note = (value: number | number[]): this => {
this.values["note"] = value; this.values["note"] = value;
return this; return this;
}; };
sustain = (value: number): this => { sustain = (value: number | number[]): this => {
this.values["sustain"] = value; this.values["sustain"] = value;
return this; 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; this.values["channel"] = value;
return this; return this;
}; };
port = (value: number | string): this => { port = (value: number | string | number[] | string[]): this => {
this.values["port"] = this.midiConnection.getMidiOutputIndex(value); 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; return this;
}; };
@ -75,37 +85,46 @@ export class MidiEvent extends AudibleEvent {
}; };
update = (): void => { update = (): void => {
const [note, bend] = noteFromPc( // Get key, pitch, parsedScale and octave from this.values object
this.values.key || "C4", const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]);
this.values.pitch || 0,
this.values.parsedScale || "MAJOR", const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]);
this.values.octave || 0
); events.forEach((event) => {
this.values.note = note; const [note, bend] = noteFromPc(
this.values.freq = midiToFreq(note); event.key as number || "C4",
if (bend) this.values.bend = bend; 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 => { out = (): void => {
function play(event: MidiEvent, params?: MidiParams): void { function play(event: MidiEvent, params: MidiParams): void {
const paramChannel = params && params.channel ? params.channel : 0; const channel = params.channel ? params.channel : 0;
const channel = event.values.channel ? event.values.channel : paramChannel; const velocity = params.velocity ? params.velocity : 100;
const velocity = event.values.velocity ? event.values.velocity : 100; const note = params.note ? params.note : 60;
const paramNote = params && params.note ? params.note : 60;
const note = event.values.note ? event.values.note : paramNote;
const sustain = event.values.sustain const sustain = params.sustain
? event.values.sustain * ? params.sustain *
event.app.clock.pulse_duration * event.app.clock.pulse_duration *
event.app.api.ppqn() event.app.api.ppqn()
: 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 bend = params.bend ? params.bend : undefined;
const port = event.values.port
? event.midiConnection.getMidiOutputIndex(event.values.port)
: event.midiConnection.getCurrentMidiPortIndex();
const port = params.port
? event.midiConnection.getMidiOutputIndex(params.port)
: event.midiConnection.getCurrentMidiPortIndex() || 0;
event.midiConnection.sendMidiNote( event.midiConnection.sendMidiNote(
note, note,
channel, channel,
@ -116,13 +135,11 @@ export class MidiEvent extends AudibleEvent {
); );
} }
if(this.values.chord) { const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[];
this.values.chord.forEach((p: MidiParams) => {
play(this, p); events.forEach((p: MidiParams) => {
}); play(this,p);
} else { });
play(this);
}
}; };
} }

View File

@ -4,11 +4,11 @@ import { Event } from "./AbstractEvents";
export class RestEvent extends Event { export class RestEvent extends Event {
constructor(length: number, app: Editor) { constructor(length: number, app: Editor) {
super(app); super(app);
this.values["length"] = length; this.values["noteLength"] = length;
} }
_fallbackMethod = (): Event => { _fallbackMethod = (): Event => {
return RestEvent.createRestProxy(this.values["length"], this.app); return RestEvent.createRestProxy(this.values["noteLength"], this.app);
}; };
public static createRestProxy = ( public static createRestProxy = (

View File

@ -1,5 +1,6 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic";
import { import {
chord as parseChord, chord as parseChord,
midiToFreq, midiToFreq,
@ -12,22 +13,23 @@ import {
// @ts-ignore // @ts-ignore
} from "superdough"; } from "superdough";
type EventObj = { [key: string]: number | number[] };
export type SoundParams = { export type SoundParams = {
dur: number; dur: number | number[];
s?: string; s?: undefined | string | string[];
}; n?: undefined | number | number[];
analyze?: boolean;
type ValuesType = { note?: number | number[];
s: string | string[]; freq?: number | number[];
n?: string | string[]; pitch?: number | number[];
dur: number; key?: string;
analyze: boolean; scale?: string;
parsedScale?: number[];
octave?: number | number[];
}; };
export class SoundEvent extends AudibleEvent { export class SoundEvent extends AudibleEvent {
nudge: number; nudge: number;
sound: any;
private methodMap = { private methodMap = {
volume: ["volume", "vol"], volume: ["volume", "vol"],
@ -276,7 +278,7 @@ export class SoundEvent extends AudibleEvent {
}; };
constructor(sound: string | string[] | object, public app: Editor) { constructor(sound: string | string[] | SoundParams, public app: Editor) {
super(app); super(app);
this.nudge = app.dough_nudge / 100; this.nudge = app.dough_nudge / 100;
@ -294,15 +296,15 @@ export class SoundEvent extends AudibleEvent {
this.values = this.processSound(sound); this.values = this.processSound(sound);
} }
private processSound = (sound: string | string[] | object): ValuesType => { private processSound = (sound: string | string[] | SoundParams | SoundParams[]): SoundParams => {
if (Array.isArray(sound)) { if (Array.isArray(sound) && typeof sound[0] === 'string') {
const s: string[] = []; const s: string[] = [];
const n: string[] = []; const n: number[] = [];
sound.forEach(str => { sound.forEach(str => {
const parts = str.split(":"); const parts = (str as string).split(":");
s.push(parts[0]); s.push(parts[0]);
if (parts[1]) { if (parts[1]) {
n.push(parts[1]); n.push(parseInt(parts[1]));
} }
}); });
return { return {
@ -312,16 +314,17 @@ export class SoundEvent extends AudibleEvent {
analyze: true analyze: true
}; };
} else if (typeof sound === 'object') { } else if (typeof sound === 'object') {
console.log(sound) const validatedObj: SoundParams = {
const validatedObj: ValuesType = {
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true, analyze: true,
...sound as Partial<ValuesType> ...sound as Partial<SoundParams>
}; };
return validatedObj; return validatedObj;
} else { } else {
if (sound.includes(":")) { if (sound.includes(":")) {
const [s, n] = sound.split(":"); const vals = sound.split(":");
const s = vals[0];
const n = parseInt(vals[1]);
return { return {
s, s,
n, n,
@ -334,8 +337,7 @@ export class SoundEvent extends AudibleEvent {
} }
} }
private updateValue<T>(key: string, value: T | T[] | SoundParams[] | null): this {
private updateValue<T>(key: string, value: T | T[] | null): this {
if (value == null) return this; if (value == null) return this;
this.values[key] = value; this.values[key] = value;
return this; return this;
@ -356,61 +358,31 @@ export class SoundEvent extends AudibleEvent {
}; };
update = (): void => { update = (): void => {
const [note, _] = noteFromPc( const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]);
this.values.key || "C4", const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]);
this.values.pitch || 0,
this.values.parsedScale || "MAJOR", events.forEach((event) => {
this.values.octave || 0 const [note, _] = noteFromPc(
); event.key as number || "C4",
this.values.freq = midiToFreq(note); event.pitch as number || 0,
event.parsedScale as number[] || event.scale || "MAJOR",
event.octave as number || 0
);
event.note = note;
event.freq = midiToFreq(note);
});
const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams;
this.values.note = newArrays.note;
this.values.freq = newArrays.freq;
}; };
generateEvents = (input: EventObj): EventObj[] => { public chord = (value: string) => {
const keys = Object.keys(input);
const maxLength = Math.max(
...keys.map((k) =>
Array.isArray(input[k]) ? (input[k] as number[]).length : 1
)
);
const output: EventObj[] = [];
for (let i = 0; i < maxLength; i++) {
const event: EventObj = {};
for (const k of keys) {
if (Array.isArray(input[k])) {
// @ts-ignore
event[k] = input[k][i % (input[k] as number[]).length];
} else {
event[k] = input[k];
}
}
output.push(event);
}
return output;
};
public chord = (
value: string | object[] | number[] | number,
...kwargs: number[]
) => {
if (typeof value === "string") {
const chord = parseChord(value); const chord = parseChord(value);
value = chord.map((note: number) => { return this.updateValue("note", chord);
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);
}; };
public invert = (howMany: number = 0) => { public invert = (howMany: number = 0) => {
if (this.values.chord) { if (this.values.chord) {
let notes = this.values.chord.map( let notes = this.values.chord.map(
@ -438,17 +410,13 @@ export class SoundEvent extends AudibleEvent {
} }
}; };
out = (): void => { out = (): void => {
const input = this.values.chord || this.values; const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]);
const events = this.generateEvents(input);
console.log(events);
for (const event of events) { for (const event of events) {
superdough( superdough(
{ ...event, ...{ freq: event.freq, dur: this.values.dur } }, event,
this.nudge, this.nudge,
this.values.dur event.dur
); );
} }
}; };

View File

@ -5,6 +5,7 @@ import { SkipEvent } from "./SkipEvent";
import { SoundEvent, SoundParams } from "./SoundEvent"; import { SoundEvent, SoundParams } from "./SoundEvent";
import { MidiEvent, MidiParams } from "./MidiEvent"; import { MidiEvent, MidiParams } from "./MidiEvent";
import { RestEvent } from "./RestEvent"; import { RestEvent } from "./RestEvent";
import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
export type InputOptions = { [key: string]: string | number }; export type InputOptions = { [key: string]: string | number };
@ -139,37 +140,39 @@ export class Player extends Event {
}; };
sound(name?: string) { sound(name?: string) {
if (this.areWeThereYet()) { if (this.areWeThereYet()) {
const event = this.next() as Pitch | Chord | ZRest; const event = this.next() as Pitch | Chord | ZRest;
const noteLengthInSeconds = this.app.clock.convertPulseToSecond(event.duration*4*this.app.clock.ppqn); const noteLengthInSeconds = this.app.clock.convertPulseToSecond(event.duration*4*this.app.clock.ppqn);
if (event instanceof Pitch) { if (event instanceof Pitch) {
const obj = event.getExisting( const obj = event.getExisting(
"freq", "freq",
"note",
"pitch", "pitch",
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale"
); ) as SoundParams;
if(event.sound) name = event.sound as string; 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; obj.dur = noteLengthInSeconds;
return new SoundEvent(obj, this.app).sound(name || "sine"); return new SoundEvent(obj, this.app).sound(name || "sine");
} else if (event instanceof Chord) { } else if (event instanceof Chord) {
const pitches = event.pitches.map((p) => { const pitches = event.pitches.map((p) => {
return p.getExisting( return p.getExisting(
"freq", "freq",
"note",
"pitch", "pitch",
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale"
); );
}); }) as SoundParams[];
const sound: SoundParams = {dur: noteLengthInSeconds}; const add = { dur: noteLengthInSeconds } as SoundParams;
if(name) sound.s = name; if(name) add.s = name;
return new SoundEvent(sound, this.app).chord(pitches); let sound = arrayOfObjectsToObjectWithArrays(pitches,add) as SoundParams;
return new SoundEvent(sound, this.app);
} else if (event instanceof ZRest) { } else if (event instanceof ZRest) {
return RestEvent.createRestProxy(event.duration, this.app); return RestEvent.createRestProxy(event.duration, this.app);
} }
@ -189,16 +192,17 @@ export class Player extends Event {
"scale", "scale",
"octave", "octave",
"parsedScale", "parsedScale",
); ) as MidiParams;
if (event instanceof Pitch) { 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); const note = new MidiEvent(obj, this.app);
return value ? note.note(value) : note; return value ? note.note(value) : note;
} else if (event instanceof ZRest) { } else if (event instanceof ZRest) {
return RestEvent.createRestProxy(event.duration, this.app); return RestEvent.createRestProxy(event.duration, this.app);
} else if (event instanceof Chord) { } else if (event instanceof Chord) {
const pitches = event.midiChord() as MidiParams[]; 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 { } else {
return SkipEvent.createSkipProxy(); return SkipEvent.createSkipProxy();