Merge branch 'main' of https://github.com/Bubobubobubobubo/Topos into logicaltime

This commit is contained in:
2023-08-26 11:10:32 +03:00
7 changed files with 778 additions and 503 deletions

View File

@ -22,21 +22,230 @@ interface ControlChange {
value: number;
}
// ======================================================================
// Array prototype extensions: easier work with lists
// ======================================================================
declare global {
interface Array<T> {
palindrome(): T[];
random(index: number): T;
rand(index: number): T;
degrade(amount: number): T;
repeatAll(amount: number): T;
repeatPair(amount: number): T;
repeatOdd(amount: number): T;
loop(index: number): T;
div(division: number): T;
shuffle(): this;
rotate(steps: number): this;
unique(): this;
}
}
Array.prototype.shuffle = function () {
/**
* Shuffles the array in place.
*
* @returns The shuffled array
*/
let currentIndex = this.length,
randomIndex;
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * 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 = Math.random() * 100;
if (rand < amount) {
if (this.length > 1) {
this.splice(i, 1);
} else {
return this;
}
} else {
i++;
}
}
return this;
};
Array.prototype.repeatAll = function <T>(this: T[], amount: number) {
/**
* 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.repeatPair = function <T>(this: T[], amount: number) {
/**
* 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.repeatOdd = function <T>(this: T[], amount: number) {
/**
* 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 (index) {
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);
};
// @ts-ignore
Array.prototype.random = function (index) {
return this[Math.floor(Math.random() * this.length)];
};
// @ts-ignore
Array.prototype.loop = function (index) {
Array.prototype.loop = function <T>(this: T[], index: number): T {
/**
* 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];
};
// @ts-ignore
Array.prototype.random = function () {
/**
* Returns a random element from the array.
*
* @returns A random element from the array
*/
return this[Math.floor(Math.random() * this.length)];
};
Array.prototype.rand = Array.prototype.random;
/**
@ -51,19 +260,16 @@ Array.prototype.in = function <T>(this: T[], value: T): boolean {
return this.includes(value);
};
async function loadSamples() {
// const ds = "https://raw.githubusercontent.com/felixroos/dough-samples/main/";
export async function loadSamples() {
return Promise.all([
initAudioOnFirstClick(),
samples("github:Bubobubobubobubo/Topos-Samples/main"),
samples("github:tidalcycles/Dirt-Samples/master").then(() =>
registerSynthSounds()
),
samples("github:Bubobubobubobubo/Topos-Samples/main"),
]);
}
loadSamples();
export const generateCacheKey = (...args: any[]): string => {
return args.map((arg) => JSON.stringify(arg)).join(",");
};
@ -92,6 +298,10 @@ export class UserAPI {
//this.load = samples("github:tidalcycles/Dirt-Samples/master");
}
_all_samples = (): object => {
return soundMap.get();
};
_reportError = (error: any): void => {
console.log(error);
clearTimeout(this.errorTimeoutID);
@ -236,7 +446,11 @@ export class UserAPI {
}
};
public midi = (value: number|object = 60): NoteEvent => {
public midi = (
value: number | object = 60,
velocity?: number,
channel?: number
): NoteEvent => {
/**
* Sends a MIDI note to the current MIDI output.
*
@ -244,6 +458,24 @@ export class UserAPI {
* @param options - an object containing options for that note
* { 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;
}
if (channel !== undefined) {
if (typeof value === "number") {
value = { note: value };
}
// @ts-ignore
value["channel"] = channel;
}
return new NoteEvent(value, this.app);
};
@ -323,8 +555,8 @@ export class UserAPI {
this.app.api.patternCache.set(key, player);
}
if ((player && player.notStarted()) || player.played) {
player.callTime = this.epulse();
player.played = false;
player.callTime = this.epulse();
player.played = false;
}
return player;
};
@ -702,6 +934,30 @@ export class UserAPI {
// Probability functions
// =============================================================
public prob = (p: number): boolean => {
/**
* Returns true p% of the time.
*
* @param p - The probability of returning true
* @returns True p% of the time
*/
return this.randomGen() * 100 < p;
};
public toss = (): boolean => {
/**
* Returns true 50% of the time.
*
* @returns True 50% of the time
* @see sometimes
* @see rarely
* @see often
* @see almostAlways
* @see almostNever
*/
return this.randomGen() > 0.5;
};
public odds = (n: number, sec: number = 15): boolean => {
/**
* Returns true n% of the time.
@ -865,6 +1121,10 @@ export class UserAPI {
return this.app.clock.pulses_since_origin;
};
// =============================================================
// Time Filters
// =============================================================
onbar = (n: number, ...bar: number[]): boolean => {
// n is acting as a modulo on the bar number
const bar_list = [...Array(n).keys()].map((i) => i + 1);
@ -889,7 +1149,7 @@ export class UserAPI {
let integral_part = Math.floor(b);
let decimal_part = b - integral_part;
final_pulses.push(
integral_part === this.app.clock.time_position.beat &&
integral_part === this.app.clock.time_position.beat &&
this.app.clock.time_position.pulse ===
decimal_part * this.app.clock.ppqn
);
@ -897,30 +1157,6 @@ export class UserAPI {
return final_pulses.some((p) => p == true);
};
prob = (p: number): boolean => {
/**
* Returns true p% of the time.
*
* @param p - The probability of returning true
* @returns True p% of the time
*/
return this.randomGen() * 100 < p;
};
toss = (): boolean => {
/**
* Returns true 50% of the time.
*
* @returns True 50% of the time
* @see sometimes
* @see rarely
* @see often
* @see almostAlways
* @see almostNever
*/
return this.randomGen() > 0.5;
};
public min = (...values: number[]): number => {
/**
* Returns the minimum value of a list of numbers.
@ -1235,13 +1471,12 @@ export class UserAPI {
// Trivial functions
// =============================================================
sound = (sound: string|object) => {
sound = (sound: string | object) => {
return new SoundEvent(sound, this.app);
};
snd = this.sound;
samples = samples;
soundMap = soundMap;
log = console.log;

File diff suppressed because it is too large Load Diff

View File

@ -8,10 +8,10 @@ export class RestEvent extends Event {
}
_fallbackMethod = (): Event => {
return this;
return RestEvent.createRestProxy(this.values["duration"], this.app);
}
public static createRestProxy = (duration: number, app: Editor) => {
public static createRestProxy = (duration: number, app: Editor): RestEvent => {
const instance = new RestEvent(duration, app);
return new Proxy(instance, {
// @ts-ignore

View File

@ -1,10 +1,10 @@
export class SkipEvent {
_fallbackMethod = (): SkipEvent => {
return this;
return SkipEvent.createSkipProxy();
}
public static createSkipProxy = () => {
public static createSkipProxy = (): SkipEvent => {
const instance = new SkipEvent();
return new Proxy(instance, {
// @ts-ignore

View File

@ -1,6 +1,6 @@
import { type Editor } from '../main';
import { AudibleEvent } from './AbstractEvents';
import { midiToFreq, noteFromPc } from 'zifferjs';
import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents";
import { midiToFreq, noteFromPc } from "zifferjs";
import {
superdough,
@ -8,7 +8,6 @@ import {
} from "superdough";
export class SoundEvent extends AudibleEvent {
constructor(sound: string | object, public app: Editor) {
super(app);
if (typeof sound === "string") this.values = { s: sound, dur: 0.5 };

View File

@ -14,10 +14,10 @@ import { indentWithTab } from "@codemirror/commands";
import { vim } from "@replit/codemirror-vim";
import { AppSettings, Universe } from "./AppSettings";
import { editorSetup } from "./EditorSetup";
import { documentation } from "./Documentation";
import { documentation_factory } from "./Documentation";
import { EditorView } from "codemirror";
import { Clock } from "./Clock";
import { UserAPI } from "./API";
import { loadSamples, UserAPI } from "./API";
import "./style.css";
import {
Universes,
@ -27,9 +27,6 @@ import {
} from "./AppSettings";
import { tryEvaluate } from "./Evaluator";
type Documentation = { [key: string]: string };
const Docs: Documentation = documentation;
// Importing showdown and setting up the markdown converter
import showdown from "showdown";
showdown.setFlavor("github");
@ -37,8 +34,8 @@ import showdownHighlight from "showdown-highlight";
const classMap = {
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-8 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-8 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
ul: "text-underline",
li: "ml-12 list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 lg:pl-4 my-2 leading-normal",
ul: "text-underline pl-6",
li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal",
p: "lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 leading-normal",
a: "lg:text-2xl text-base text-orange-300",
code: "lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden",
@ -66,7 +63,7 @@ export class Editor {
universes: Universes = template_universes;
selected_universe: string;
local_index: number = 1;
editor_mode: "global" | "local" | "init" | "notes" = "local";
editor_mode: "global" | "local" | "init" | "notes" = "global";
fontSize: Compartment;
withLineNumbers: Compartment;
vimModeCompartment: Compartment;
@ -78,6 +75,7 @@ export class Editor {
userPlugins: Extension[] = [];
state: EditorState;
api: UserAPI;
docs: { [key: string]: string } = {};
// Audio stuff
audioContext: AudioContext;
@ -92,19 +90,19 @@ export class Editor {
// Transport elements
play_buttons: HTMLButtonElement[] = [
document.getElementById("play-button-1") as HTMLButtonElement,
document.getElementById("play-button-2") as HTMLButtonElement,
//document.getElementById("play-button-2") as HTMLButtonElement,
];
pause_buttons: HTMLButtonElement[] = [
document.getElementById("pause-button-1") as HTMLButtonElement,
document.getElementById("pause-button-2") as HTMLButtonElement,
//document.getElementById("pause-button-2") as HTMLButtonElement,
];
stop_buttons: HTMLButtonElement[] = [
document.getElementById("stop-button-1") as HTMLButtonElement,
document.getElementById("stop-button-2") as HTMLButtonElement,
//document.getElementById("stop-button-2") as HTMLButtonElement,
];
clear_buttons: HTMLButtonElement[] = [
document.getElementById("clear-button-1") as HTMLButtonElement,
document.getElementById("clear-button-2") as HTMLButtonElement,
//document.getElementById("clear-button-2") as HTMLButtonElement,
];
documentation_button: HTMLButtonElement = document.getElementById(
"doc-button-1"
@ -235,31 +233,13 @@ export class Editor {
];
let dynamicPlugins = new Compartment();
this.state = EditorState.create({
extensions: [
...this.editorExtensions,
EditorView.lineWrapping,
dynamicPlugins.of(this.userPlugins),
Prec.highest(
keymap.of([
{
key: "Ctrl-Enter",
run: () => {
return true;
},
},
])
),
keymap.of([indentWithTab]),
],
doc: this.universes[this.selected_universe].locals[this.local_index]
.candidate,
});
this.view = new EditorView({
parent: document.getElementById("editor") as HTMLElement,
state: this.state,
// ================================================================================
// Building the documentation
loadSamples().then(() => {
this.docs = documentation_factory(this);
});
// ================================================================================
// ================================================================================
// Application event listeners
@ -579,6 +559,33 @@ export class Editor {
(globalThis as Record<string, any>)[name] = value;
});
this.state = EditorState.create({
extensions: [
...this.editorExtensions,
EditorView.lineWrapping,
dynamicPlugins.of(this.userPlugins),
Prec.highest(
keymap.of([
{
key: "Ctrl-Enter",
run: () => {
return true;
},
},
])
),
keymap.of([indentWithTab]),
],
doc: this.universes[this.selected_universe].global.candidate,
});
this.view = new EditorView({
parent: document.getElementById("editor") as HTMLElement,
state: this.state,
});
this.changeModeFromInterface("global");
// Loading from URL bar
let url = new URLSearchParams(window.location.search);
if (url !== undefined) {
@ -660,10 +667,11 @@ export class Editor {
const converter = new showdown.Converter({
emoji: true,
moreStyling: true,
backslashEscapesHTMLTags: true,
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
});
const converted_markdown = converter.makeHtml(
Docs[this.currentDocumentationPane]
this.docs[this.currentDocumentationPane]
);
function wrapCodeWithPre(inputString: string): string {
let newString = inputString.replace(/<code>/g, "<pre><code>");