Updating ziffers and adding generator functions and tonnetz support

This commit is contained in:
2023-11-11 00:32:04 +02:00
parent 086fe54c72
commit d98b3bb791
8 changed files with 154 additions and 18 deletions

View File

@ -0,0 +1,469 @@
import { type UserAPI } from "../API";
import { safeScale, stepsToScale } from "zifferjs";
export { };
declare global {
interface Array<T> {
add(amount: number): number[];
sub(amount: number): number[];
mult(amount: number): number[];
div(amount: number): number[];
mouseX(): T[],
mouseY(): T[],
palindrome(): T[];
random(index: number): T;
rand(index: number): T;
degrade(amount: number): T;
repeat(amount: number): T;
repeatEven(amount: number): T;
repeatOdd(amount: number): T;
beat(division: number): T;
dur(durations: number[]): T;
b(division: number): T;
bar(): T;
pick(): T;
loop(index: number): T;
shuffle(): this;
scale(name: string, base_note?: number): this;
scaleArp(scaleName: string): this;
rotate(steps: number): this;
unique(): this;
square(): number[];
sqrt(): number[];
gen(min: number, max: number, times: number): number[];
sometimes(func: Function): number[];
apply(func: Function): number[];
}
}
export const makeArrayExtensions = (api: UserAPI) => {
Array.prototype.mouseX = function <T>(this: T[]): T {
/**
* @returns Value from list based on mouse X position
*/
const mouse_position = api.mouseX();
const screenWidth = window.innerWidth;
const zoneWidth = screenWidth / this.length;
const zoneIndex = Math.floor(mouse_position / zoneWidth);
return this[zoneIndex];
};
Array.prototype.mouseY = function <T>(this: T[]): T {
const mouse_position = api.mouseY();
const screenHeight = window.innerHeight;
const zoneHeight = screenHeight / this.length;
const zoneIndex = Math.floor(mouse_position / zoneHeight);
return this[zoneIndex];
};
Array.prototype.square = function(): number[] {
/**
* @returns New array with squared values.
*/
return this.map((x: number) => x * x);
};
Array.prototype.sometimes = function(func: Function): number[] {
if (api.randomGen() < 0.5) {
return func(this);
} else {
return this;
}
};
Array.prototype.apply = function(func: Function): number[] {
return func(this);
};
Array.prototype.sqrt = function(): number[] {
/**
* @returns New array with square roots of values. Throws if any element is negative.
*/
if (this.some((x) => x < 0))
throw new Error("Cannot take square root of negative number");
return this.map((x: number) => Math.sqrt(x));
};
Array.prototype.add = function(amount: number): number[] {
/**
* @param amount - The value to add to each element in the array.
* @returns New array with added values.
*/
return this.map((x: number) => x + amount);
};
Array.prototype.sub = function(amount: number): number[] {
/**
* @param amount - The value to subtract from each element in the array.
* @returns New array with subtracted values.
*/
return this.map((x: number) => x - amount);
};
Array.prototype.mult = function(amount: number): number[] {
/**
* @param amount - The value to multiply with each element in the array.
* @returns New array with multiplied values.
*/
return this.map((x: number) => x * amount);
};
Array.prototype.div = function(amount: number): number[] {
/**
* @param amount - The value to divide each element in the array by.
* @returns New array with divided values. Throws if division by zero.
*/
if (amount === 0) throw new Error("Division by zero");
return this.map((x: number) => x / amount);
};
Array.prototype.pick = function() {
/**
* Returns a random element from an array.
*
* @param array - The array of values to pick from
*/
return this[Math.floor(api.randomGen() * this.length)];
};
Array.prototype.gen = function(min: number, max: number, times: number) {
/**
* Returns an array of random numbers.
* @param min - The minimum value of the random numbers
* @param max - The maximum value of the random numbers
* @param times - The number of random numbers to generate
* @returns An array of random numbers
*/
if (times < 1) {
return [];
}
return Array.from(
{ length: times },
() => Math.floor(api.randomGen() * (max - min + 1)) + min
);
};
Array.prototype.bar = function(value: number = 1) {
/**
* Returns an element from an array based on the current bar.
*
* @param array - The array of values to pick from
*/
if (value === 1) {
return this[api.app.clock.time_position.bar % this.length];
} else {
return this[
Math.floor(api.app.clock.time_position.bar / value) % this.length
];
}
};
Array.prototype.beat = function(divisor: number = 1) {
const chunk_size = divisor; // Get the first argument (chunk size)
const timepos = api.app.clock.pulses_since_origin;
const slice_count = Math.floor(
timepos / Math.floor(chunk_size * api.ppqn())
);
return this[slice_count % this.length];
};
Array.prototype.b = Array.prototype.beat;
// Array.prototype.dur = function(...durations) {
// const timepos = api.app.clock.pulses_since_origin;
// const ppqn = api.ppqn();
// let adjustedDurations = [];
// for (let i = 0; i < this.length; i++) {
// adjustedDurations.push(durations[i % durations.length]);
// }
// let cumulativeDuration = 0;
// let totalDuration = adjustedDurations.reduce((acc, duration) => acc + duration * ppqn, 0);
// let modulatedTimePos = timepos % totalDuration;
// for (let i = 0; i < this.length; i++) {
// const valueDuration = adjustedDurations[i] as any * ppqn;
// if (modulatedTimePos < cumulativeDuration + valueDuration) {
// return this[i];
// }
// cumulativeDuration += valueDuration;
// }
// // This point should not be reached if durations are correctly specified
// throw new Error('Durations array does not match the pattern length.');
// };
Array.prototype.dur = function(...durations) {
const timepos = api.app.clock.pulses_since_origin;
const ppqn = api.ppqn();
const adjustedDurations = this.map((_, index) => durations[index % durations.length]);
// @ts-ignore
const totalDurationInPulses = adjustedDurations.reduce((acc, duration) => acc + duration * ppqn, 0);
const loopPosition = timepos % totalDurationInPulses;
let cumulativeDuration = 0;
for (let i = 0; i < this.length; i++) {
const valueDurationInPulses = adjustedDurations[i] as any * ppqn;
cumulativeDuration += valueDurationInPulses;
if (loopPosition < cumulativeDuration) {
return this[i];
}
}
throw new Error('Durations array does not match the pattern length.');
};
Array.prototype.shuffle = function() {
/**
* Shuffles the array in place.
*
* @returns The shuffled array
*/
let currentIndex = this.length,
randomIndex;
while (currentIndex !== 0) {
randomIndex = Math.floor(api.randomGen() * currentIndex);
currentIndex--;
[this[currentIndex], this[randomIndex]] = [
this[randomIndex],
this[currentIndex],
];
}
return this;
};
Array.prototype.rotate = function(steps: number) {
/**
* Rotates the array in place.
*
* @param steps - The number of steps to rotate the array by
* @returns The rotated array
*/
const length = this.length;
if (steps < 0) {
steps = length + (steps % length);
} else if (steps > 0) {
steps = steps % length;
} else {
return this;
}
const rotated = this.splice(-steps, steps);
this.unshift(...rotated);
return this;
};
Array.prototype.unique = function() {
/**
* Removes duplicate elements from the array in place.
*
* @returns The array without duplicates
*/
const seen = new Set();
let writeIndex = 0;
for (let readIndex = 0; readIndex < this.length; readIndex++) {
const value = this[readIndex];
if (!seen.has(value)) {
seen.add(value);
this[writeIndex++] = value;
}
}
this.length = writeIndex;
return this;
};
Array.prototype.degrade = function <T>(this: T[], amount: number) {
/**
* Removes elements from the array at random. If the array has
* only one element left, it will not be removed.
*
* @param amount - The amount of elements to remove
* @returns The degraded array
*/
if (amount < 0 || amount > 100) {
throw new Error("Amount should be between 0 and 100");
}
if (this.length <= 1) {
return this;
}
for (let i = 0; i < this.length;) {
const rand = api.randomGen() * 100;
if (rand < amount) {
if (this.length > 1) {
this.splice(i, 1);
} else {
return this;
}
} else {
i++;
}
}
return this;
};
Array.prototype.repeat = function <T>(this: T[], amount: number = 1) {
/**
* Repeats all elements in the array n times.
*
* @param amount - The amount of times to repeat the elements
* @returns The repeated array
*/
if (amount < 1) {
throw new Error("Amount should be at least 1");
}
let result = [];
for (let i = 0; i < this.length; i++) {
for (let j = 0; j < amount; j++) {
result.push(this[i]);
}
}
this.length = 0;
this.push(...result);
return this;
};
Array.prototype.repeatOdd = function <T>(this: T[], amount: number = 1) {
/**
* Repeats all elements in the array n times, except for the
* elements at odd indexes.
*
* @param amount - The amount of times to repeat the elements
* @returns The repeated array
*/
if (amount < 1) {
throw new Error("Amount should be at least 1");
}
let result = [];
for (let i = 0; i < this.length; i++) {
// If the index is even, repeat the element
if (i % 2 === 0) {
for (let j = 0; j < amount; j++) {
result.push(this[i]);
}
} else {
result.push(this[i]);
}
}
this.length = 0;
this.push(...result);
return this;
};
Array.prototype.repeatEven = function <T>(this: T[], amount: number = 1) {
/**
* Repeats all elements in the array n times, except for the
* elements at even indexes.
*
* @param amount - The amount of times to repeat the elements
* @returns The repeated array
*
* @remarks
* This function is the opposite of repeatPair.
*/
if (amount < 1) {
throw new Error("Amount should be at least 1");
}
let result = [];
for (let i = 0; i < this.length; i++) {
// If the index is odd, repeat the element
if (i % 2 !== 0) {
for (let j = 0; j < amount; j++) {
result.push(this[i]);
}
} else {
result.push(this[i]);
}
}
// Update the original array
this.length = 0;
this.push(...result);
return this;
};
// @ts-ignore
Array.prototype.palindrome = function <T>() {
/**
* Returns a palindrome of the array.
*
* @returns The palindrome of the array
*/
let left_to_right = Array.from(this);
let right_to_left = Array.from(this.reverse());
return left_to_right.concat(right_to_left);
};
Array.prototype.loop = function(index: number) {
/**
* Returns an element from the array based on the index.
* The index will wrap over the array.
*
* @param index - The index of the element to return
* @returns The element at the given index
*/
return this[index % this.length];
};
Array.prototype.random = function() {
/**
* Returns a random element from the array.
*
* @returns A random element from the array
*/
return this[Math.floor(api.randomGen() * this.length)];
};
Array.prototype.rand = Array.prototype.random;
};
Array.prototype.scale = function(
scale: string = "major",
base_note: number = 0
) {
/**
* @param scale - the scale name
* @param base_note - the base note to start at (MIDI note number)
*
* @returns notes from the desired scale
*/
// This is a helper function to handle up or down octaviation.
const mod = (n: number, m: number) => ((n % m) + m) % m;
const selected_scale = stepsToScale(safeScale(scale));
return this.map((value) => {
const octaveShift = Math.floor(value / selected_scale.length) * 12;
return (
selected_scale[mod(value, selected_scale.length)] +
base_note +
octaveShift
);
});
};
Array.prototype.scaleArp = function(
scaleName: string = "major",
boundary: number = 0
) {
/*
* @param scaleName - the scale name
* @param mask - the length of the mask
*
* @returns arpeggiated notes from the scale
*/
const scale = stepsToScale(safeScale(scaleName));
let result = [];
boundary = boundary > scale.length ? scale.length : boundary;
boundary = boundary == 0 ? scale.length : boundary;
for (let j = 0; j < boundary; j++) {
for (let i = 0; i < this.length; i++) {
result.push(this[i] + scale[j]);
}
}
return result;
};

View File

@ -0,0 +1,98 @@
import { type UserAPI } from "../API";
import { Player } from "../classes/ZPlayer";
declare global {
interface Number {
z(): Player;
z0(): Player;
z1(): Player;
z2(): Player;
z3(): Player;
z4(): Player;
z5(): Player;
z6(): Player;
z7(): Player;
z8(): Player;
z9(): Player;
z10(): Player;
z11(): Player;
z12(): Player;
z13(): Player;
z14(): Player;
z15(): Player;
z16(): Player;
note(): number;
}
}
export const makeNumberExtensions = (api: UserAPI) => {
Number.prototype.z0 = function (options: {[key: string]: any} = {}) {
return api.z0(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z1 = function (options: {[key: string]: any} = {}) {
return api.z1(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z2 = function (options: {[key: string]: any} = {}) {
return api.z2(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z3 = function (options: {[key: string]: any} = {}) {
return api.z3(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z4 = function (options: {[key: string]: any} = {}) {
return api.z4(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z5 = function (options: {[key: string]: any} = {}) {
return api.z5(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z6 = function (options: {[key: string]: any} = {}) {
return api.z6(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z7 = function (options: {[key: string]: any} = {}) {
return api.z7(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z8 = function (options: {[key: string]: any} = {}) {
return api.z8(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z9 = function (options: {[key: string]: any} = {}) {
return api.z9(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z10 = function (options: {[key: string]: any} = {}) {
return api.z10(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z11 = function (options: {[key: string]: any} = {}) {
return api.z11(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z12 = function (options: {[key: string]: any} = {}) {
return api.z12(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z13 = function (options: {[key: string]: any} = {}) {
return api.z13(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z14 = function (options: {[key: string]: any} = {}) {
return api.z14(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z15 = function (options: {[key: string]: any} = {}) {
return api.z15(this.valueOf().toString().split("").join(), options);
};
Number.prototype.z16 = function (options: {[key: string]: any} = {}) {
return api.z16(this.valueOf().toString().split("").join(), options);
};
}

View File

@ -0,0 +1,229 @@
import { noteNameToMidi } from "zifferjs";
import { type UserAPI } from "../API";
import { Player } from "../classes/ZPlayer";
export {};
// Extend String prototype
declare global {
interface String {
speak(): void;
rate(speed: number): string;
pitch(pitch: number): string;
volume(volume: number): string;
voice(voice: number): string;
lang(language: string): string;
options(): SpeechOptions;
z(): Player;
z0(): Player;
z1(): Player;
z2(): Player;
z3(): Player;
z4(): Player;
z5(): Player;
z6(): Player;
z7(): Player;
z8(): Player;
z9(): Player;
z10(): Player;
z11(): Player;
z12(): Player;
z13(): Player;
z14(): Player;
z15(): Player;
z16(): Player;
note(): number;
}
}
const isJsonString = (str: string):boolean => {
return str[0] === '{' && str[str.length - 1] === '}'
}
const stringObject = (str: string, params: object) => {
if(isJsonString(str)) {
const obj = JSON.parse(str);
return JSON.stringify({...obj, ...params});
} else {
return JSON.stringify({...params, text: str});
}
}
export const makeStringExtensions = (api: UserAPI) => {
String.prototype.speak = function () {
const options = JSON.parse(this.valueOf());
new Speaker({ ...options, text: options.text }).speak().then(() => {
// Done
}).catch((e) => {
console.log("Error speaking:", e);
});
};
String.prototype.rate = function (speed: number) {
return stringObject(this.valueOf(), {rate: speed});
};
String.prototype.pitch = function (pitch: number) {
return stringObject(this.valueOf(), {pitch: pitch});
};
String.prototype.lang = function (language: string) {
return stringObject(this.valueOf(),{lang: language});
};
String.prototype.volume = function (volume: number) {
return stringObject(this.valueOf(), {volume: volume});
};
String.prototype.voice = function (voice: number) {
return stringObject(this.valueOf(), {voice: voice});
};
String.prototype.z = function (options: {[key: string]: any} = {}) {
return api.z(this.valueOf(), options);
};
String.prototype.z0 = function (options: {[key: string]: any} = {}) {
return api.z0(this.valueOf(), options);
};
String.prototype.z1 = function (options: {[key: string]: any} = {}) {
return api.z1(this.valueOf(), options);
};
String.prototype.z2 = function (options: {[key: string]: any} = {}) {
return api.z2(this.valueOf(), options);
};
String.prototype.z3 = function (options: {[key: string]: any} = {}) {
return api.z3(this.valueOf(), options);
};
String.prototype.z4 = function (options: {[key: string]: any} = {}) {
return api.z4(this.valueOf(), options);
};
String.prototype.z5 = function (options: {[key: string]: any} = {}) {
return api.z5(this.valueOf(), options);
};
String.prototype.z6 = function (options: {[key: string]: any} = {}) {
return api.z6(this.valueOf(), options);
};
String.prototype.z7 = function (options: {[key: string]: any} = {}) {
return api.z7(this.valueOf(), options);
};
String.prototype.z8 = function (options: {[key: string]: any} = {}) {
return api.z8(this.valueOf(), options);
};
String.prototype.z9 = function (options: {[key: string]: any} = {}) {
return api.z9(this.valueOf(), options);
};
String.prototype.z10 = function (options: {[key: string]: any} = {}) {
return api.z10(this.valueOf(), options);
};
String.prototype.z11 = function (options: {[key: string]: any} = {}) {
return api.z11(this.valueOf(), options);
};
String.prototype.z12 = function (options: {[key: string]: any} = {}) {
return api.z12(this.valueOf(), options);
};
String.prototype.z13 = function (options: {[key: string]: any} = {}) {
return api.z13(this.valueOf(), options);
};
String.prototype.z14 = function (options: {[key: string]: any} = {}) {
return api.z14(this.valueOf(), options);
};
String.prototype.z15 = function (options: {[key: string]: any} = {}) {
return api.z15(this.valueOf(), options);
};
String.prototype.z16 = function (options: {[key: string]: any} = {}) {
return api.z16(this.valueOf(), options);
};
String.prototype.note = function () {
try {
return parseInt(this.valueOf());
} catch (e) {
return noteNameToMidi(this.valueOf());
}
};
}
type SpeechOptions = {
text?: string;
rate?: number;
pitch?: number;
volume?: number;
voice?: number;
lang?: string;
}
let speakerTimeout: number;
export class Speaker {
constructor(
public options: SpeechOptions
) {}
speak = () => {
return new Promise<void>((resolve, reject) => {
if (this.options.text) {
const synth = window.speechSynthesis;
if(synth.speaking) synth.cancel();
const utterance = new SpeechSynthesisUtterance(this.options.text);
utterance.rate = this.options.rate || 1;
utterance.pitch = this.options.pitch || 1;
utterance.volume = this.options.volume || 1;
if (this.options.voice) {
utterance.voice = synth.getVoices()[this.options.voice];
}
if(this.options.lang) {
// Check if language has country code
if (this.options.lang.length === 2) {
utterance.lang = `${this.options.lang}-${this.options.lang.toUpperCase()}`
} else if (this.options.lang.length === 5) {
utterance.lang = this.options.lang;
} else {
// Fallback to en us
utterance.lang = 'en-US';
}
}
utterance.onend = () => {
resolve();
};
utterance.onerror = (error) => {
reject(error);
};
if(synth.speaking) {
// Cancel again?
synth.cancel();
// Set timeout
if(speakerTimeout) clearTimeout(speakerTimeout);
speakerTimeout = setTimeout(() => {
synth.speak(utterance);
}, 200);
} else {
synth.speak(utterance);
}
} else {
reject("No text provided");
}
});
}
}