From 0a6d77986749d3e0a0276a0fbaf144b014bc9e97 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 14 Apr 2024 21:15:36 +0200 Subject: [PATCH] Begin breaking up the API file --- src/API.ts | 2678 ---------------------------- src/API/API.ts | 652 +++++++ src/API/Cache.ts | 60 + src/API/Canvas.ts | 414 +++++ src/API/Console.ts | 20 + src/API/Counter.ts | 40 + src/API/Drunk.ts | 37 + src/API/Filters.ts | 203 +++ src/API/LFO.ts | 65 + src/API/MIDI.ts | 199 +++ src/API/Math.ts | 36 + src/API/Mouse.ts | 33 + src/API/OSC.ts | 27 + src/API/Probabilities.ts | 53 + src/API/Randomness.ts | 33 + src/API/Script.ts | 63 + src/API/Sound.ts | 43 + src/API/Theme.ts | 30 + src/API/Transport.ts | 76 + src/API/Warp.ts | 16 + src/API/Ziffers.ts | 73 + src/DOM/UILogic.ts | 2 +- src/Docs/patterns/functions.ts | 27 - src/IO/MidiConnection.ts | 36 +- src/classes/SoundEvent.ts | 2 +- src/extensions/ArrayExtensions.ts | 2 +- src/extensions/NumberExtensions.ts | 2 +- src/extensions/StringExtensions.ts | 2 +- src/main.ts | 2 +- 29 files changed, 2197 insertions(+), 2729 deletions(-) delete mode 100644 src/API.ts create mode 100644 src/API/API.ts create mode 100644 src/API/Cache.ts create mode 100644 src/API/Canvas.ts create mode 100644 src/API/Console.ts create mode 100644 src/API/Counter.ts create mode 100644 src/API/Drunk.ts create mode 100644 src/API/Filters.ts create mode 100644 src/API/LFO.ts create mode 100644 src/API/MIDI.ts create mode 100644 src/API/Math.ts create mode 100644 src/API/Mouse.ts create mode 100644 src/API/OSC.ts create mode 100644 src/API/Probabilities.ts create mode 100644 src/API/Randomness.ts create mode 100644 src/API/Script.ts create mode 100644 src/API/Sound.ts create mode 100644 src/API/Theme.ts create mode 100644 src/API/Transport.ts create mode 100644 src/API/Warp.ts create mode 100644 src/API/Ziffers.ts diff --git a/src/API.ts b/src/API.ts deleted file mode 100644 index 5ad143b..0000000 --- a/src/API.ts +++ /dev/null @@ -1,2678 +0,0 @@ -import { sendToServer, type OSCMessage, oscMessages } from "./IO/OSC"; -import { getAllScaleNotes, nearScales, seededRandom } from "zifferjs"; -import colorschemes from "./Editor/colors.json"; -import { - MidiCCEvent, - MidiConnection, - MidiNoteEvent, -} from "./IO/MidiConnection"; -import { tryEvaluate, evaluateOnce } from "./Evaluator"; -import { DrunkWalk } from "./Utils/Drunk"; -import { Editor } from "./main"; -import { SoundEvent } from "./Classes/SoundEvent"; -import { MidiEvent, MidiParams } from "./Classes/MidiEvent"; -import { LRUCache } from "lru-cache"; -import { InputOptions, Player } from "./Classes/ZPlayer"; -import { isGenerator, isGeneratorFunction, maybeToNumber } from "./Utils/Generic"; -import { - loadUniverse, - openUniverseModal, - template_universes, -} from "./Editor/FileManagement"; -import { - samples, - initAudioOnFirstClick, - registerSynthSounds, - registerZZFXSounds, - soundMap, - // @ts-ignore -} from "superdough"; -import { Speaker } from "./Extensions/StringExtensions"; -import { getScaleNotes } from "zifferjs"; -import { OscilloscopeConfig } from "./DOM/Visuals/Oscilloscope"; -import { blinkScript } from "./DOM/Visuals/Blinkers"; -import { SkipEvent } from "./Classes/SkipEvent"; -import { AbstractEvent, EventOperation } from "./Classes/AbstractEvents"; -import drums from "./tidal-drum-machines.json"; -import { ShapeObject, createConicGradient, createLinearGradient, createRadialGradient, drawBackground, drawBall, drawBalloid, drawDonut, drawEquilateral, drawImage, drawPie, drawSmiley, drawStar, drawStroke, drawText, drawTriangular } from "./DOM/Visuals/CanvasVisuals"; - -interface ControlChange { - channel: number; - control: number; - value: number; -} - -export async function loadSamples() { - return Promise.all([ - initAudioOnFirstClick(), - samples("github:tidalcycles/Dirt-Samples/master", undefined, { - tag: "Tidal", - }).then(() => registerSynthSounds()), - registerZZFXSounds(), - samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", { - tag: "Machines", - }), - samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, { - tag: "FoxDot", - }), - samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, { - tag: "Pack", - }), - samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, { - tag: "Amiga", - }), - samples("github:Bubobubobubobubo/Dough-Juj/main", undefined, { - tag: "Juliette", - }), - samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, { - tag: "Amen", - }), - samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, { - tag: "Waveforms", - }), - ]); -} - -export class UserAPI { - /** - * The UserAPI class is the interface between the user's code and the backend. It provides - * access to the AudioContext, to the MIDI Interface, to internal variables, mouse position, - * useful functions, etc... This is the class that is exposed to the user's action and any - * function destined to the user should be placed here. - */ - - public codeExamples: { [key: string]: string } = {}; - private counters: { [key: string]: any } = {}; - private _drunk: DrunkWalk = new DrunkWalk(-100, 100, false); - public randomGen = Math.random; - public currentSeed: string | undefined = undefined; - public localSeeds = new Map(); - public patternCache = new LRUCache({ max: 10000, ttl: 10000 * 60 * 5 }); - public invalidPatterns: { [key: string]: boolean } = {}; - public cueTimes: { [key: string]: number } = {}; - private errorTimeoutID: number = 0; - private printTimeoutID: number = 0; - public MidiConnection: MidiConnection; - public scale_aid: string | number | undefined = undefined; - public hydra: any; - public onceEvaluator: boolean = true; - public forceEvaluator: boolean = false; - - load: samples; - public global: { [key: string]: any }; - - constructor(public app: Editor) { - this.MidiConnection = new MidiConnection(this, app.settings); - this.global = {}; - this.g = this.global; - } - - public g: any; - - _loadUniverseFromInterface = (universe: string) => { - this.app.selected_universe = universe.trim(); - this.app.settings.selected_universe = universe.trim(); - loadUniverse(this.app, universe as string); - openUniverseModal(); - }; - - _deleteUniverseFromInterface = (universe: string) => { - delete this.app.universes[universe]; - if (this.app.settings.selected_universe === universe) { - this.app.settings.selected_universe = "Welcome"; - this.app.selected_universe = "Welcome"; - } - this.app.settings.saveApplicationToLocalStorage( - this.app.universes, - this.app.settings, - ); - this.app.updateKnownUniversesView(); - }; - - _playDocExample = (code?: string) => { - /** - * Play an example from the documentation. The example is going - * to be stored in the example buffer belonging to the universe. - * This buffer is going to be cleaned everytime the user press - * pause or leaves the documentation window. - * - * @param code - The code example to play (identifier) - */ - let current_universe = this.app.universes[this.app.selected_universe]; - this.app.exampleIsPlaying = true; - if (!current_universe.example) { - current_universe.example = { - candidate: "", - committed: "", - evaluations: 0, - }; - current_universe.example.candidate! = code - ? code - : (this.app.selectedExample as string); - } else { - current_universe.example.candidate! = code - ? code - : (this.app.selectedExample as string); - } - this.clearPatternCache(); - this.stop(); - this.play(); - }; - - _stopDocExample = () => { - let current_universe = this.app.universes[this.app.selected_universe]; - if (current_universe?.example !== undefined) { - this.app.exampleIsPlaying = false; - current_universe.example.candidate! = ""; - current_universe.example.committed! = ""; - } - this.clearPatternCache(); - this.stop(); - }; - - _playDocExampleOnce = (code?: string) => { - let current_universe = this.app.universes[this.app.selected_universe]; - if (current_universe?.example !== undefined) { - current_universe.example.candidate! = ""; - current_universe.example.committed! = ""; - } - this.clearPatternCache(); - this.stop(); - this.play(); - this.app.exampleIsPlaying = true; - evaluateOnce(this.app, code as string); - }; - - _all_samples = (): object => { - return soundMap.get(); - }; - - _reportError = (error: any): void => { - const extractLineAndColumn = (error: Error) => { - const stackLines = error.stack?.split("\n"); - if (stackLines) { - for (const line of stackLines) { - if (line.includes("")) { - const match = line.match(/:(\d+):(\d+)/); - if (match) - return { - line: parseInt(match[1], 10), - column: parseInt(match[2], 10), - }; - } - } - } - return { line: null, column: null }; - }; - - const { line, column } = extractLineAndColumn(error); - const errorMessage = - line && column - ? `${error.message} (Line: ${line - 2}, Column: ${column})` - : error.message; - - clearTimeout(this.errorTimeoutID); - clearTimeout(this.printTimeoutID); - this.app.interface.error_line.innerHTML = errorMessage; - this.app.interface.error_line.style.color = "red"; - this.app.interface.error_line.classList.remove("hidden"); - // @ts-ignore - this.errorTimeoutID = setTimeout( - () => this.app.interface.error_line.classList.add("hidden"), - 2000, - ); - }; - - _logMessage = (message: any, error: boolean = false): void => { - console.log(message); - clearTimeout(this.printTimeoutID); - clearTimeout(this.errorTimeoutID); - this.app.interface.error_line.innerHTML = message as string; - this.app.interface.error_line.style.color = error ? "red" : "white"; - this.app.interface.error_line.classList.remove("hidden"); - // @ts-ignore - this.printTimeoutID = setTimeout( - () => this.app.interface.error_line.classList.add("hidden"), - 4000, - ); - }; - - // ============================================================= - // Time functions - // ============================================================= - - public time = (): number => { - /** - * @returns the current AudioContext time (wall clock) - */ - return this.app.audioContext.currentTime; - }; - - public play = (): void => { - this.app.setButtonHighlighting("play", true); - this.MidiConnection.sendStartMessage(); - this.app.clock.start(); - }; - - public pause = (): void => { - this.app.setButtonHighlighting("pause", true); - this.app.clock.pause(); - }; - - public stop = (): void => { - this.app.setButtonHighlighting("stop", true); - this.app.clock.stop(); - }; - silence = this.stop; - hush = this.stop; - - // ============================================================= - // Time warp functions - // ============================================================= - - public warp = (n: number): void => { - /** - * Time-warp the clock by using the tick you wish to jump to. - */ - this.app.clock.tick = n; - this.app.clock.time_position = this.app.clock.convertTicksToTimeposition(n); - }; - - public beat_warp = (beat: number): void => { - /** - * Time-warp the clock by using the tick you wish to jump to. - */ - this.app.clock.tick = beat * this.app.clock.ppqn; - this.app.clock.time_position = this.app.clock.convertTicksToTimeposition( - beat * this.app.clock.ppqn, - ); - }; - - // ============================================================= - // Mouse functions - // ============================================================= - - onmousemove = (e: MouseEvent) => { - this.app._mouseX = e.pageX; - this.app._mouseY = e.pageY; - }; - - public mouseX = (): number => { - /** - * @returns The current x position of the mouse - */ - return this.app._mouseX; - }; - - public mouseY = (): number => { - /** - * @returns The current y position of the mouse - */ - return this.app._mouseY; - }; - - public noteX = (): number => { - /** - * @returns The current x position scaled to 0-127 using screen width - */ - return Math.floor((this.app._mouseX / document.body.clientWidth) * 127); - }; - - public noteY = (): number => { - /** - * @returns The current y position scaled to 0-127 using screen height - */ - return Math.floor((this.app._mouseY / document.body.clientHeight) * 127); - }; - - // ============================================================= - // Utility functions - // ============================================================= - - script = (...args: number[]): void => { - /** - * Evaluates 1-n local script(s) - * - * @param args - The scripts to evaluate - * @returns The result of the evaluation - */ - args.forEach((arg) => { - if (arg >= 1 && arg <= 9) { - blinkScript(this.app, "local", arg); - tryEvaluate( - this.app, - this.app.universes[this.app.selected_universe].locals[arg], - ); - } - }); - }; - s = this.script; - - delete_script = (script: number): void => { - /** - * Clears a local script - * - * @param script - The script to clear - */ - this.app.universes[this.app.selected_universe].locals[script] = { - candidate: "", - committed: "", - evaluations: 0, - }; - }; - cs = this.delete_script; - - copy_script = (from: number, to: number): void => { - /** - * Copy from a local script to another local script - * - * @param from - The script to copy from - * @param to - The script to copy to - */ - this.app.universes[this.app.selected_universe].locals[to] = { - ...this.app.universes[this.app.selected_universe].locals[from], - }; - }; - cps = this.copy_script; - - copy_universe = (from: string, to: string): void => { - this.app.universes[to] = { - ...this.app.universes[from], - }; - }; - - delete_universe = (universe: string): void => { - if (this.app.selected_universe === universe) { - this.app.selected_universe = "Default"; - } - delete this.app.universes[universe]; - this.app.settings.saveApplicationToLocalStorage( - this.app.universes, - this.app.settings, - ); - this.app.updateKnownUniversesView(); - }; - - big_bang = (): void => { - /** - * Clears all universes - * TODO: add documentation. This doesn't work super well. - */ - if (confirm("Are you sure you want to delete all universes?")) { - this.app.universes = { - ...template_universes, - }; - this.app.settings.saveApplicationToLocalStorage( - this.app.universes, - this.app.settings, - ); - } - this.app.selected_universe = "Default"; - this.app.updateKnownUniversesView(); - }; - - // ============================================================= - // MIDI related functions - // ============================================================= - - public midi_outputs = (): void => { - /** - * Prints a list of available MIDI outputs in the console. - * - * @returns A list of available MIDI outputs - */ - this._logMessage(this.MidiConnection.listMidiOutputs(), false); - }; - - public midi_output = (outputName: string): void => { - /** - * Switches the MIDI output to the specified output. - * - * @param outputName - The name of the MIDI output to switch to - */ - if (!outputName) { - console.log(this.MidiConnection.getCurrentMidiPort()); - } else { - this.MidiConnection.switchMidiOutput(outputName); - } - }; - - public midi = ( - 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. - * - * @param note - the MIDI note number to send - * @param options - an object containing options for that note - * { channel: 0, velocity: 100, duration: 0.5 } - */ - - const event = { note: value, velocity, channel, port } as MidiParams; - - return new MidiEvent(event, this.app); - }; - - public sysex = (data: Array): void => { - /** - * Sends a MIDI sysex message to the current MIDI output. - * - * @param data - The sysex data to send - */ - this.MidiConnection.sendSysExMessage(data); - }; - - public pitch_bend = (value: number, channel: number): void => { - /** - * Sends a MIDI pitch bend to the current MIDI output. - * - * @param value - The value of the pitch bend - * @param channel - The MIDI channel to send the pitch bend on - * - * @returns The value of the pitch bend - */ - this.MidiConnection.sendPitchBend(value, channel); - }; - - public program_change = (program: number, channel: number): void => { - /** - * Sends a MIDI program change to the current MIDI output. - * - * @param program - The MIDI program to send - * @param channel - The MIDI channel to send the program change on - */ - this.MidiConnection.sendProgramChange(program, channel); - }; - - public midi_clock = (): void => { - /** - * Sends a MIDI clock to the current MIDI output. - */ - this.MidiConnection.sendMidiClock(); - }; - - public control_change = ({ - control = 20, - value = 0, - channel = 0, - }: ControlChange): void => { - /** - * Sends a MIDI control change to the current MIDI output. - * - * @param control - The MIDI control to send - * @param value - The value of the control - */ - this.MidiConnection.sendMidiControlChange(control, value, channel); - }; - public cc = this.control_change; - - public midi_panic = (): void => { - /** - * Sends a MIDI panic message to the current MIDI output. - */ - this.MidiConnection.panic(); - }; - - public active_note_events = ( - channel?: number, - ): MidiNoteEvent[] | undefined => { - /** - * @returns A list of currently active MIDI notes - */ - let events; - if (channel) { - events = this.MidiConnection.activeNotesFromChannel(channel); - } else { - events = this.MidiConnection.activeNotes; - } - if (events.length > 0) return events; - else return undefined; - }; - - public transmission(): boolean { - /** - * Returns true if there are active notes - */ - return this.MidiConnection.activeNotes.length > 0; - } - - public active_notes = (channel?: number): number[] | undefined => { - /** - * @returns A list of currently active MIDI notes - */ - const notes = this.active_note_events(channel); - if (notes && notes.length > 0) return notes.map((e) => e.note); - else return undefined; - }; - - public kill_active_notes = (): void => { - /** - * Clears all active notes - */ - this.MidiConnection.activeNotes = []; - }; - - public sticky_notes = (channel?: number): number[] | undefined => { - /** - * - * @param channel - * @returns - */ - let notes; - if (channel) notes = this.MidiConnection.stickyNotesFromChannel(channel); - else notes = this.MidiConnection.stickyNotes; - if (notes.length > 0) return notes.map((e) => e.note); - else return undefined; - }; - - public kill_sticky_notes = (): void => { - /** - * Clears all sticky notes - */ - this.MidiConnection.stickyNotes = []; - }; - - public buffer = (channel?: number): boolean => { - /** - * Return true if there is last note event - */ - if (channel) - return ( - this.MidiConnection.findNoteFromBufferInChannel(channel) !== undefined - ); - else return this.MidiConnection.noteInputBuffer.length > 0; - }; - - public buffer_event = (channel?: number): MidiNoteEvent | undefined => { - /** - * @returns Returns latest unlistened note event - */ - if (channel) - return this.MidiConnection.findNoteFromBufferInChannel(channel); - else return this.MidiConnection.noteInputBuffer.shift(); - }; - - public buffer_note = (channel?: number): number | undefined => { - /** - * @returns Returns latest received note - */ - const note = this.buffer_event(channel); - return note ? note.note : undefined; - }; - - public last_note_event = (channel?: number): MidiNoteEvent | undefined => { - /** - * @returns Returns last received note - */ - if (channel) return this.MidiConnection.lastNoteInChannel[channel]; - else return this.MidiConnection.lastNote; - }; - - public last_note = (channel?: number): number => { - /** - * @returns Returns last received note - */ - const note = this.last_note_event(channel); - return note ? note.note : 60; - }; - - public ccIn = (control: number, channel?: number): number => { - /** - * @returns Returns last received cc - */ - if (channel) { - if (this.MidiConnection.lastCCInChannel[channel]) { - return this.MidiConnection.lastCCInChannel[channel][control]; - } else return 0; - } else return this.MidiConnection.lastCC[control] || 0; - }; - - public has_cc = (channel?: number): boolean => { - /** - * Return true if there is last cc event - */ - if (channel) - return ( - this.MidiConnection.findCCFromBufferInChannel(channel) !== undefined - ); - else return this.MidiConnection.ccInputBuffer.length > 0; - }; - - public buffer_cc = (channel?: number): MidiCCEvent | undefined => { - /** - * @returns Returns latest unlistened cc event - */ - if (channel) return this.MidiConnection.findCCFromBufferInChannel(channel); - else return this.MidiConnection.ccInputBuffer.shift(); - }; - - public show_scale = ( - root: number | string, - scale: number | string, - channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0, - soundOff: boolean = false, - ): void => { - /** - * Sends given scale to midi output for visual aid - */ - if (!this.scale_aid || scale !== this.scale_aid) { - this.hide_scale(root, scale, channel, port); - const scaleNotes = getAllScaleNotes(scale, root); - // Send each scale note to current midi out - scaleNotes.forEach((note) => { - this.MidiConnection.sendMidiOn(note, channel, 1, port); - if (soundOff) this.MidiConnection.sendAllSoundOff(channel, port); - }); - - this.scale_aid = scale; - } - }; - - public hide_scale = ( - // @ts-ignore - root: number | string = 0, - // @ts-ignore - scale: number | string = 0, - channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0, - ): void => { - /** - * Hides all notes by sending all notes off to midi output - */ - const allNotes = Array.from(Array(128).keys()); - // Send each scale note to current midi out - allNotes.forEach((note) => { - this.MidiConnection.sendMidiOff(note, channel, port); - }); - this.scale_aid = undefined; - }; - - midi_notes_off = ( - channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0, - ): void => { - /** - * Sends all notes off to midi output - */ - this.MidiConnection.sendAllNotesOff(channel, port); - }; - - midi_sound_off = ( - channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0, - ): void => { - /** - * Sends all sound off to midi output - */ - this.MidiConnection.sendAllSoundOff(channel, port); - }; - - // ============================================================= - // Cache functions - // ============================================================= - - public generateCacheKey = (...args: any[]): string => { - return args.map((arg) => JSON.stringify(arg)).join(","); - }; - - public resetAllFromCache = (): void => { - this.patternCache.forEach((player) => (player as Player).reset()); - }; - - public clearPatternCache = (): void => { - this.patternCache.clear(); - } - - public removePatternFromCache = (id: string): void => { - this.patternCache.delete(id); - }; - - cache = (key: string, value: any) => { - /** - * Gets or sets a value in the cache. - * - * @param key - The key of the value to get or set - * @param value - The value to set - * @returns The value of the key - */ - if (value !== undefined) { - if (isGenerator(value)) { - if (this.patternCache.has(key)) { - const cachedValue = (this.patternCache.get(key) as Generator).next().value - if (cachedValue !== 0 && !cachedValue) { - const generator = value as unknown as Generator - this.patternCache.set(key, generator); - return maybeToNumber(generator.next().value); - } - return maybeToNumber(cachedValue); - } else { - const generator = value as unknown as Generator - this.patternCache.set(key, generator); - return maybeToNumber(generator.next().value); - } - } else if (isGeneratorFunction(value)) { - if (this.patternCache.has(key)) { - const cachedValue = (this.patternCache.get(key) as Generator).next().value; - if (cachedValue || cachedValue === 0 || cachedValue === 0n) { - return maybeToNumber(cachedValue); - } else { - const generator = value(); - this.patternCache.set(key, generator); - return maybeToNumber(generator.next().value); - } - } else { - const generator = value(); - this.patternCache.set(key, generator); - return maybeToNumber(generator.next().value); - } - } else { - this.patternCache.set(key, value); - return maybeToNumber(value); - } - } else { - return maybeToNumber(this.patternCache.get(key)); - } - } - - // ============================================================= - // Ziffers related functions - // ============================================================= - - public z = ( - input: string | Generator, - options: InputOptions = {}, - id: number | string = "", - ): Player => { - const zid = "z" + id.toString(); - const key = id === "" ? this.generateCacheKey(input, options) : zid; - - const validSyntax = typeof input === "string" && !this.invalidPatterns[input] - - let player; - let replace = false; - - if (this.app.api.patternCache.has(key)) { - player = this.app.api.patternCache.get(key) as Player; - - if (typeof input === "string" && - player.input !== input && - (player.atTheBeginning() || this.forceEvaluator)) { - replace = true; - } - } - - if ((typeof input !== "string" || validSyntax) && (!player || replace)) { - if (typeof input === "string" && player && this.forceEvaluator) { - // If pattern change is forced in the middle of the cycle - if (!player.updatePattern(input, options)) { - this.logOnce(`Invalid syntax: ${input}`); - }; - this.forceEvaluator = false; - } else { - // If pattern is not in cache or is to be replaced - const newPlayer = player ? new Player(input, options, this.app, zid, player.nextEndTime()) : new Player(input, options, this.app, zid); - if (newPlayer.isValid()) { - player = newPlayer - this.patternCache.set(key, player); - } else if (typeof input === "string") { - this.invalidPatterns[input] = true; - } - } - } - - if (player) { - - if (player.atTheBeginning()) { - if (typeof input === "string" && !validSyntax) this.app.api.log(`Invalid syntax: ${input}`); - } - - if (player.ziffers.generator && player.ziffers.generatorDone) { - this.removePatternFromCache(key); - } - - if (typeof id === "number") player.zid = zid; - - player.updateLastCallTime(); - - if (id !== "" && zid !== "z0") { - // Sync named patterns to z0 by default - player.sync("z0", false); - } - - return player; - } else { - throw new Error(`Invalid syntax: ${input}`); - } - }; - - public z0 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 0); - public z1 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 1); - public z2 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 2); - public z3 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 3); - public z4 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 4); - public z5 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 5); - public z6 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 6); - public z7 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 7); - public z8 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 8); - public z9 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 9); - public z10 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 10); - public z11 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 11); - public z12 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 12); - public z13 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 13); - public z14 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 14); - public z15 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 15); - public z16 = (input: string, opts: InputOptions = {}) => - this.z(input, opts, 16); - - // ============================================================= - // Counter and iteration - // ============================================================= - - public once = (): boolean => { - /** - * Returns true if the code is being evaluated for the first time. - * - * @returns True if the code is being evaluated for the first time - */ - const firstTime = this.app.api.onceEvaluator; - this.app.api.onceEvaluator = false; - - return firstTime; - } - - public counter = ( - name: string | number, - limit?: number, - step?: number, - ): number => { - /** - * Returns the current value of a counter, and increments it by the step value. - * - * @param name - The name of the counter - * @param limit - The upper limit of the counter - * @param step - The step value of the counter - * @returns The current value of the counter - */ - - if (!(name in this.counters)) { - // Create new counter with default step of 1 - this.counters[name] = { - value: 0, - step: step ?? 1, - limit, - }; - } else { - // Check if limit has changed - if (this.counters[name].limit !== limit) { - // Reset value to 0 and update limit - this.counters[name].value = 0; - this.counters[name].limit = limit; - } - - // Check if step has changed - if (this.counters[name].step !== step) { - // Update step - this.counters[name].step = step ?? this.counters[name].step; - } - - // Increment existing iterator by step value - this.counters[name].value += this.counters[name].step; - - // Check for limit overshoot - if ( - this.counters[name].limit !== undefined && - this.counters[name].value > this.counters[name].limit - ) { - this.counters[name].value = 0; - } - } - - // Return current iterator value - return this.counters[name].value; - }; - $ = this.counter; - count = this.counter; - - // ============================================================= - // Iterator functions (for loops, with evaluation count, etc...) - // ============================================================= - - i = (n?: number) => { - /** - * Returns the current iteration of global file. - * - * @returns The current iteration of global file - */ - if (n !== undefined) { - this.app.universes[this.app.selected_universe].global.evaluations = n; - return this.app.universes[this.app.selected_universe]; - } - return this.app.universes[this.app.selected_universe].global - .evaluations as number; - }; - - // ============================================================= - // Drunk mechanism - // ============================================================= - - public drunk = (n?: number) => { - /** - * - * This function sets or returns the current drunk - * mechanism's value. - * - * @param n - [optional] The value to set the drunk mechanism to - * @returns The current value of the drunk mechanism - */ - if (n !== undefined) { - this._drunk.position = n; - return this._drunk.getPosition(); - } - this._drunk.step(); - return this._drunk.getPosition(); - }; - - public drunk_max = (max: number) => { - /** - * Sets the maximum value of the drunk mechanism. - * - * @param max - The maximum value of the drunk mechanism - */ - this._drunk.max = max; - }; - - public drunk_min = (min: number) => { - /** - * Sets the minimum value of the drunk mechanism. - * - * @param min - The minimum value of the drunk mechanism - */ - this._drunk.min = min; - }; - - public drunk_wrap = (wrap: boolean) => { - /** - * Sets whether the drunk mechanism should wrap around - * - * @param wrap - Whether the drunk mechanism should wrap around - */ - this._drunk.toggleWrap(wrap); - }; - - // ============================================================= - // Randomness functions - // ============================================================= - - randI = (min: number, max: number): number => { - /** - * Returns a random integer between min and max. - * - * @param min - The minimum value of the random number - * @param max - The maximum value of the random number - * @returns A random integer between min and max - */ - return Math.floor(this.randomGen() * (max - min + 1)) + min; - }; - - rand = (min: number, max: number): number => { - /** - * Returns a random float between min and max. - * - * @param min - The minimum value of the random number - * @param max - The maximum value of the random number - * @returns A random float between min and max - */ - return this.randomGen() * (max - min) + min; - }; - - irand = this.randI; - rI = this.randI; - r = this.rand; - ir = this.randI; - - seed = (seed: string | number): void => { - /** - * Seed the random numbers globally in UserAPI. - * @param seed - The seed to use - */ - if (typeof seed === "number") seed = seed.toString(); - if (this.currentSeed !== seed) { - this.currentSeed = seed; - this.randomGen = seededRandom(seed); - } - }; - - localSeededRandom = (seed: string | number): Function => { - if (typeof seed === "number") seed = seed.toString(); - if (this.localSeeds.has(seed)) return this.localSeeds.get(seed) as Function; - const newSeededRandom = seededRandom(seed); - this.localSeeds.set(seed, newSeededRandom); - return newSeededRandom; - }; - - clearLocalSeed = (seed: string | number | undefined = undefined): void => { - if (seed) this.localSeeds.delete(seed.toString()); - this.localSeeds.clear(); - }; - - // ============================================================= - // Quantification functions - // ============================================================= - - public quantize = (value: number, quantization: number[]): number => { - /** - * Returns the closest value in an array to a given value. - * - * @param value - The value to quantize - * @param quantization - The array of values to quantize to - * @returns The closest value in the array to the given value - */ - if (quantization.length === 0) { - return value; - } - let closest = quantization[0]; - quantization.forEach((q) => { - if (Math.abs(q - value) < Math.abs(closest - value)) { - closest = q; - } - }); - return closest; - }; - quant = this.quantize; - - public clamp = (value: number, min: number, max: number): number => { - /** - * Returns a value clamped between min and max. - * - * @param value - The value to clamp - * @param min - The minimum value of the clamped value - * @param max - The maximum value of the clamped value - * @returns A value clamped between min and max - */ - return Math.min(Math.max(value, min), max); - }; - cmp = this.clamp; - - // ============================================================= - // 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, beats: number = 1): boolean => { - /** - * Returns true n% of the time. - * - * @param n - The probability of returning true. 1/4 = 25% = 0.25, 80/127 = 62.9% = 0.6299212598425197, etc... - * @param beats - The time frame in beats - * @returns True n% of the time - */ - return this.randomGen() < (n * this.ppqn()) / (this.ppqn() * beats); - }; - - // @ts-ignore - public never = (beats: number = 1): boolean => { - /** - * Returns false - * @param beats - Doesn't give a * about beats - * @returns False - */ - return false; - }; - - public almostNever = (beats: number = 1): boolean => { - /** - * Returns true 2.5% of the time in given time frame. - * - * @param beats - The time frame in beats - * @returns True 2.5% of the time - */ - return this.randomGen() < (0.025 * this.ppqn()) / (this.ppqn() * beats); - }; - - public rarely = (beats: number = 1): boolean => { - /** - * Returns true 10% of the time. - * - * @param beats - The time frame in beats - * @returns True 10% of the time. - */ - return this.randomGen() < (0.1 * this.ppqn()) / (this.ppqn() * beats); - }; - - public scarcely = (beats: number = 1): boolean => { - /** - * Returns true 25% of the time. - * - * @param beats - The time frame in beats - * @returns True 25% of the time - */ - return this.randomGen() < (0.25 * this.ppqn()) / (this.ppqn() * beats); - }; - - public sometimes = (beats: number = 1): boolean => { - /** - * Returns true 50% of the time. - * - * @param beats - The time frame in beats - * @returns True 50% of the time - */ - return this.randomGen() < (0.5 * this.ppqn()) / (this.ppqn() * beats); - }; - - public often = (beats: number = 1): boolean => { - /** - * Returns true 75% of the time. - * - * @param beats - The time frame in beats - * @returns True 75% of the time - */ - return this.randomGen() < (0.75 * this.ppqn()) / (this.ppqn() * beats); - }; - - public frequently = (beats: number = 1): boolean => { - /** - * Returns true 90% of the time. - * - * @param beats - The time frame in beats - * @returns True 90% of the time - */ - return this.randomGen() < (0.9 * this.ppqn()) / (this.ppqn() * beats); - }; - - public almostAlways = (beats: number = 1): boolean => { - /** - * Returns true 98.5% of the time. - * - * @param beats - The time frame in beats - * @returns True 98.5% of the time - */ - return this.randomGen() < (0.985 * this.ppqn()) / (this.ppqn() * beats); - }; - - // @ts-ignore - public always = (beats: number = 1): boolean => { - /** - * Returns true 100% of the time. - * @param beats - Doesn't give a * about beats - * @returns true - */ - return true; - }; - - public dice = (sides: number): number => { - /** - * Returns the value of a dice roll with n sides. - * - * @param sides - The number of sides on the dice - * @returns The value of a dice roll with n sides - */ - return Math.floor(this.randomGen() * sides) + 1; - }; - - // ============================================================= - // Time markers - // ============================================================= - - cbar = (): number => { - /** - * Returns the current bar number - * - * @returns The current bar number - */ - return this.app.clock.time_position.bar + 1; - }; - - ctick = (): number => { - /** - * Returns the current tick number - * - * @returns The current tick number - */ - return this.app.clock.tick + 1; - }; - - cpulse = (): number => { - /** - * Returns the current pulse number - * - * @returns The current pulse number - */ - return this.app.clock.time_position.pulse + 1; - }; - - cbeat = (): number => { - /** - * Returns the current beat number - * - * @returns The current beat number - */ - return this.app.clock.time_position.beat + 1; - }; - - ebeat = (): number => { - /** - * Returns the current beat number since the origin of time - */ - return this.app.clock.beats_since_origin + 1; - }; - - epulse = (): number => { - /** - * Returns the current number of pulses elapsed since origin of time - */ - return this.app.clock.pulses_since_origin + 1; - }; - - nominator = (): number => { - /** - * Returns the current nominator of the time signature - */ - return this.app.clock.time_signature[0]; - }; - - meter = (): number => { - /** - * Returns the current meter (denominator of the time signature) - */ - return this.app.clock.time_signature[1]; - }; - - denominator = this.meter; - - pulsesForBar = (): number => { - /** - * Returns the number of pulses in a given bar - */ - return (this.tempo() * this.ppqn() * this.nominator()) / 60; - } - - // ============================================================= - // Fill - // ============================================================= - - public fill = (): boolean => this.app.fill; - - // ============================================================= - // Time Filters - // ============================================================= - - public fullseq = (sequence: string, duration: number) => { - if (sequence.split("").every((c) => c === "x" || c === "o")) { - return [...sequence].map((c) => c === "x").beat(duration); - } else { - return false; - } - }; - - public seq = (expr: string, duration: number = 0.5): boolean => { - let len = expr.length * duration; - let output: number[] = []; - - for (let i = 1; i <= len + 1; i += duration) { - output.push(Math.floor(i * 10) / 10); - } - output.pop(); - - output = output.filter((_, idx) => { - const exprIdx = idx % expr.length; - return expr[exprIdx] === "x"; - }); - - return this.oncount(output, len); - }; - - public beat = (n: number | number[] = 1, nudge: number = 0): boolean => { - /** - * Determine if the current pulse is on a specified beat, with optional nudge. - * @param n Single beat multiplier or array of beat multipliers - * @param nudge Offset in pulses to nudge the beat forward or backward - * @returns True if the current pulse is on one of the specified beats (considering nudge), false otherwise - */ - const nArray = Array.isArray(n) ? n : [n]; - const results: boolean[] = nArray.map( - (value) => - (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % - Math.floor(value * this.ppqn()) === - 0, - ); - return results.some((value) => value === true); - }; - b = this.beat; - - public bar = (n: number | number[] = 1, nudge: number = 0): boolean => { - /** - * Determine if the current pulse is on a specified bar, with optional nudge. - * @param n Single bar multiplier or array of bar multipliers - * @param nudge Offset in bars to nudge the bar forward or backward - * @returns True if the current pulse is on one of the specified bars (considering nudge), false otherwise - */ - const nArray = Array.isArray(n) ? n : [n]; - const barLength = this.app.clock.time_signature[1] * this.ppqn(); - const nudgeInPulses = Math.floor(nudge * barLength); - const results: boolean[] = nArray.map( - (value) => - (this.app.clock.pulses_since_origin - nudgeInPulses) % - Math.floor(value * barLength) === - 0, - ); - return results.some((value) => value === true); - }; - B = this.bar; - - public pulse = (n: number | number[] = 1, nudge: number = 0): boolean => { - /** - * Determine if the current pulse is on a specified pulse count, with optional nudge. - * @param n Single pulse count or array of pulse counts - * @param nudge Offset in pulses to nudge the pulse forward or backward - * @returns True if the current pulse is on one of the specified pulse counts (considering nudge), false otherwise - */ - const nArray = Array.isArray(n) ? n : [n]; - const results: boolean[] = nArray.map( - (value) => (this.app.clock.pulses_since_origin - nudge) % value === 0, - ); - return results.some((value) => value === true); - }; - p = this.pulse; - - public tick = (tick: number | number[], offset: number = 0): boolean => { - const nArray = Array.isArray(tick) ? tick : [tick]; - const results: boolean[] = nArray.map( - (value) => this.app.clock.time_position.pulse === value + offset, - ); - return results.some((value) => value === true); - }; - - public dur = (n: number | number[]): boolean => { - let nums: number[] = Array.isArray(n) ? n : [n]; - // @ts-ignore - return this.beat(nums.dur(...nums)); - }; - - // ============================================================= - // Other core temporal functions - // ============================================================= - - public flip = (chunk: number, ratio: number = 50): boolean => { - /** - * Determines if the current time position is in the first - * or second half of a given time chunk. - * @param chunk Time chunk to consider - * @param ratio Optional ratio to influence the true/false output (0-100) - * @returns Whether the function returns true or false based on ratio and time chunk - */ - let realChunk = chunk * 2; - const time_pos = this.app.clock.pulses_since_origin; - const full_chunk = Math.floor(realChunk * this.ppqn()); - // const current_chunk = Math.floor(time_pos / full_chunk); - const threshold = Math.floor((ratio / 100) * full_chunk); - const pos_within_chunk = time_pos % full_chunk; - return pos_within_chunk < threshold; - }; - - public flipbar = (chunk: number = 1): boolean => { - let realFlip = chunk; - const time_pos = this.app.clock.time_position.bar; - const current_chunk = Math.floor(time_pos / realFlip); - return current_chunk % 2 === 0; - }; - - // ============================================================= - // "On" Filters - // ============================================================= - - public onbar = ( - bars: number[] | number, - n: number = this.app.clock.time_signature[0], - ): boolean => { - let current_bar = (this.app.clock.time_position.bar % n) + 1; - return typeof bars === "number" - ? bars === current_bar - : bars.some((b) => b == current_bar); - }; - - onbeat = (...beat: number[]): boolean => { - /** - * Returns true if the current beat is in the given list of beats. - * - * @remarks - * This function can also operate with decimal beats! - * - * @param beat - The beats to check - * @returns True if the current beat is in the given list of beats - */ - let final_pulses: boolean[] = []; - beat.forEach((b) => { - let beat = b % this.nominator() || this.nominator(); - let integral_part = Math.floor(beat); - integral_part = integral_part == 0 ? this.nominator() : integral_part; - let decimal_part = Math.floor((beat - integral_part) * this.ppqn() + 1); - // This was once revelead to me in a dream - if (decimal_part <= 0) - decimal_part = decimal_part + this.ppqn() * this.nominator(); - final_pulses.push( - integral_part === this.cbeat() && this.cpulse() === decimal_part, - ); - }); - return final_pulses.some((p) => p == true); - }; - - oncount = (beats: number[] | number, count: number): boolean => { - /** - * Returns true if the current beat is in the given list of beats. - * - * @remarks - * This function can also operate with decimal beats! - * - * @param beat - The beats to check - * @returns True if the current beat is in the given list of beats - */ - if (typeof beats === "number") beats = [beats]; - const origin = this.app.clock.pulses_since_origin; - let final_pulses: boolean[] = []; - beats.forEach((b) => { - b = b < 1 ? 0 : b - 1; - const beatInTicks = Math.ceil(b * this.ppqn()); - const meterPosition = origin % (this.ppqn() * count); - return final_pulses.push(meterPosition === beatInTicks); - }); - return final_pulses.some((p) => p == true); - }; - - oneuclid = (pulses: number, length: number, rotate: number = 0): boolean => { - /** - * Returns true if the current beat is in the given euclid sequence. - * @param pulses - The number of pulses in the cycle - * @param length - The length of the cycle - * @param rotate - Rotation of the euclidian sequence - * @returns True if the current beat is in the given euclid sequence - */ - const cycle = this._euclidean_cycle(pulses, length, rotate); - const beats = cycle.reduce((acc: number[], x: boolean, i: number) => { - if (x) acc.push(i + 1); - return acc; - }, []); - return this.oncount(beats, length); - }; - - // ====================================================================== - // Delay related functions - // ====================================================================== - - delay = (ms: number, func: Function): void => { - /** - * Delays the execution of a function by a given number of milliseconds. - * - * @param ms - The number of milliseconds to delay the function by - * @param func - The function to execute - * @returns The current time signature - */ - setTimeout(func, ms); - }; - - delayr = (ms: number, nb: number, func: Function): void => { - /** - * Delays the execution of a function by a given number of milliseconds, repeated a given number of times. - * - * @param ms - The number of milliseconds to delay the function by - * @param nb - The number of times to repeat the delay - * @param func - The function to execute - * @returns The current time signature - */ - const list = [...Array(nb).keys()].map((i) => ms * i); - list.forEach((ms, _) => { - setTimeout(func, ms); - }); - }; - - // ============================================================= - // Rythmic generators - // ============================================================= - - public euclid = ( - iterator: number, - pulses: number, - length: number, - rotate: number = 0, - ): boolean => { - /** - * Returns a euclidean cycle of size length, with n pulses, rotated or not. - * - * @param iterator - Iteration number in the euclidian cycle - * @param pulses - The number of pulses in the cycle - * @param length - The length of the cycle - * @param rotate - Rotation of the euclidian sequence - * @returns boolean value based on the euclidian sequence - */ - return this._euclidean_cycle(pulses, length, rotate)[iterator % length]; - }; - ec: Function = this.euclid; - - public rhythm = ( - div: number, - pulses: number, - length: number, - rotate: number = 0, - ): boolean => { - return ( - this.beat(div) && this._euclidean_cycle(pulses, length, rotate).beat(div) - ); - }; - ry = this.rhythm; - - - public nrhythm = ( - div: number, - pulses: number, - length: number, - rotate: number = 0, - ): boolean => { - let rhythm = this._euclidean_cycle(pulses, length, rotate).map(n => !n) - return ( - this.beat(div) && rhythm.beat(div) - ); - }; - nry = this.nrhythm; - - _euclidean_cycle( - pulses: number, - length: number, - rotate: number = 0, - ): boolean[] { - if (pulses == length) return Array.from({ length }, () => true); - function startsDescent(list: number[], i: number): boolean { - const length = list.length; - const nextIndex = (i + 1) % length; - return list[i] > list[nextIndex] ? true : false; - } - if (pulses >= length) return [true]; - const resList = Array.from( - { length }, - (_, i) => (((pulses * (i - 1)) % length) + length) % length, - ); - let cycle = resList.map((_, i) => startsDescent(resList, i)); - if (rotate != 0) { - cycle = cycle.slice(rotate).concat(cycle.slice(0, rotate)); - } - return cycle; - } - - bin = (iterator: number, n: number): boolean => { - /** - * Returns a binary cycle of size n. - * - * @param iterator - Iteration number in the binary cycle - * @param n - The number to convert to binary - * @returns boolean value based on the binary sequence - */ - let convert: string = n.toString(2); - let tobin: boolean[] = convert.split("").map((x: string) => x === "1"); - return tobin[iterator % tobin.length]; - }; - - public binrhythm = (div: number, n: number): boolean => { - /** - * Returns a binary cycle of size n, divided by div. - * - * @param div - The divisor of the binary cycle - * @param n - The number to convert to binary - * @returns boolean value based on the binary sequence - */ - let convert: string = n.toString(2); - let tobin: boolean[] = convert.split("").map((x: string) => x === "1"); - return this.beat(div) && tobin.beat(div); - }; - bry = this.binrhythm; - - // ============================================================= - // Low Frequency Oscillators - // ============================================================= - - public line = (start: number, end: number, step: number = 1): number[] => { - /** - * Returns an array of values between start and end, with a given step. - * - * @param start - The start value of the array - * @param end - The end value of the array - * @param step - The step value of the array - * @returns An array of values between start and end, with a given step - */ - function countPlaces(num: number) { - var text = num.toString(); - var index = text.indexOf("."); - return index == -1 ? 0 : (text.length - index - 1); - } - const result: number[] = []; - - if ((end > start && step > 0) || (end < start && step < 0)) { - for (let value = start; value <= end; value += step) { - result.push(value); - } - } else if ((end > start && step < 0) || (end < start && step > 0)) { - for (let value = start; value >= end; value -= step) { - result.push(parseFloat(value.toFixed(countPlaces(step)))); - } - } else { - console.error("Invalid range or step provided."); - } - - return result; - }; - - public sine = (freq: number = 1, phase: number = 0): number => { - /** - * Returns a sine wave between -1 and 1. - * - * @param freq - The frequency of the sine wave - * @param phase - The phase of the sine wave - * @returns A sine wave between -1 and 1 - */ - return Math.sin(2 * Math.PI * freq * (this.app.clock.ctx.currentTime - phase)); - }; - - public usine = (freq: number = 1, phase: number = 0): number => { - /** - * Returns a sine wave between 0 and 1. - * - * @param freq - The frequency of the sine wave - * @param phase - The phase of the sine wave - * @returns A sine wave between 0 and 1 - * @see sine - */ - return ((this.sine(freq, phase) + 1) / 2); - }; - - saw = (freq: number = 1, phase: number = 0): number => { - /** - * Returns a saw wave between -1 and 1. - * - * @param freq - The frequency of the saw wave - * @param phase - The phase of the saw wave - * @returns A saw wave between -1 and 1 - * @see triangle - * @see square - * @see sine - * @see noise - */ - return (((this.app.clock.ctx.currentTime * freq + phase) % 1) * 2 - 1); - }; - - usaw = (freq: number = 1, phase: number = 0): number => { - /** - * Returns a saw wave between 0 and 1. - * - * @param freq - The frequency of the saw wave - * @param offset - The offset of the saw wave - * @returns A saw wave between 0 and 1 - * @see saw - */ - return ((this.saw(freq, phase) + 1) / 2); - }; - - triangle = (freq: number = 1, phase: number = 0): number => { - /** - * Returns a triangle wave between -1 and 1. - * - * @returns A triangle wave between -1 and 1 - * @see saw - * @see square - * @see sine - * @see noise - */ - return (Math.abs(this.saw(freq, phase)) * 2 - 1); - }; - - utriangle = (freq: number = 1, phase: number = 0): number => { - /** - * Returns a triangle wave between 0 and 1. - * - * @param freq - The frequency of the triangle wave - * @param offset - The offset of the triangle wave - * @returns A triangle wave between 0 and 1 - * @see triangle - */ - return ((this.triangle(freq, phase) + 1) / 2); - }; - - square = ( - freq: number = 1, - duty: number = 0.5, - ): number => { - /** - * Returns a square wave with a specified duty cycle between -1 and 1. - * - * @returns A square wave with a specified duty cycle between -1 and 1 - * @see saw - * @see triangle - * @see sine - * @see noise - */ - const period = 1 / freq; - const t = (Date.now() / 1000) % period; - return (t / period < duty ? 1 : -1); - }; - - usquare = (freq: number = 1, duty: number = 0.5): number => { - /** - * Returns a square wave between 0 and 1. - * - * @param freq - The frequency of the square wave - * @param offset - The offset of the square wave - * @returns A square wave between 0 and 1 - * @see square - */ - return ((this.square(freq, duty) + 1) / 2); - }; - - noise = (): number => { - /** - * Returns a random value between -1 and 1. - * - * @returns A random value between -1 and 1 - * @see saw - * @see triangle - * @see square - * @see sine - * @see noise - */ - return (this.randomGen() * 2 - 1); - }; - - unoise = (): number => { - /** - * Returns a random value between 0 and 1. - * - * @returns A random value between 0 and 1 - * @see noise - */ - return ((this.noise() + 1) / 2); - }; - - // ============================================================= - // Math functions - // ============================================================= - - public min = (...values: number[]): number => { - /** - * Returns the minimum value of a list of numbers. - * - * @param values - The list of numbers - * @returns The minimum value of the list of numbers - */ - return Math.min(...values); - }; - - public max = (...values: number[]): number => { - /** - * Returns the maximum value of a list of numbers. - * - * @param values - The list of numbers - * @returns The maximum value of the list of numbers - */ - return Math.max(...values); - }; - - public mean = (...values: number[]): number => { - /** - * Returns the mean of a list of numbers. - * - * @param values - The list of numbers - * @returns The mean value of the list of numbers - */ - const sum = values.reduce( - (accumulator, currentValue) => accumulator + currentValue, - 0, - ); - return sum / values.length; - }; - - limit = (value: number, min: number, max: number): number => { - /** - * Limits a value between a minimum and a maximum. - * - * @param value - The value to limit - * @param min - The minimum value - * @param max - The maximum value - * @returns The limited value - */ - return Math.min(Math.max(value, min), max); - }; - - abs = Math.abs; - - // ============================================================= - // Speech synthesis - // ============================================================= - - speak = ( - text: string, - lang: string = "en-US", - voice: number = 0, - rate: number = 1, - pitch: number = 1, - ): void => { - /* - * Speaks the given text using the browser's speech synthesis API. - * @param text - The text to speak - * @param voice - The index of the voice to use - * @param rate - The rate at which to speak the text - * @param pitch - The pitch at which to speak the text - * - */ - const speaker = new Speaker({ - text: text, - lang: lang, - voice: voice, - rate: rate, - pitch: pitch, - }); - speaker - .speak() - .then(() => { - // Done speaking - }) - .catch((err) => { - console.log(err); - }); - }; - - // ============================================================= - // Hydra integration - // ============================================================= - - stop_hydra = (): void => { - /** - * Empties the buffer of the Hydra sketch. - */ - this.app.hydra.hush(); - }; - - // ============================================================= - // Trivial functions - // ============================================================= - - sound = (sound: string | string[] | null | undefined) => { - if (sound) return new SoundEvent(sound, this.app); - else return new SkipEvent(); - }; - - snd = this.sound; - samples = samples; - - log = (message: any) => { - console.log(message); - this._logMessage(message, false); - }; - - logOnce = (message: any) => { - if (this.onceEvaluator) { - console.log(message); - this._logMessage(message, false); - this.onceEvaluator = false; - } - } - - scale = getScaleNotes; - - nearScales = nearScales; - - rate = (rate: number): void => { - rate = rate; - // TODO: Implement this. This function should change the rate at which the global script - // is evaluated. This is useful for slowing down the script, or speeding it up. The default - // would be 1.0, which is the current rate (very speedy). - }; - - // ============================================================= - // High Order Functions - // ============================================================= - - register = (name: string, operation: EventOperation): true => { - AbstractEvent.prototype[name] = function( - this: AbstractEvent, - ...args: any[] - ) { - return operation(this, ...args); - }; - return true; - }; - - all = (operation: EventOperation): true => { - AbstractEvent.prototype.chainAll = function(...args: any[]) { - return operation(this, ...args); - }; - return true; - } - - public shuffle = (array: T[]): T[] => { - /** - * Returns a shuffled version of an array. - * @param array - The array to shuffle - * @returns A shuffled version of the array - */ - return array.sort(() => this.randomGen() - 0.5); - }; - - public reverse = (array: T[]): T[] => { - /** - * Returns a reversed version of an array. - * @param array - The array to reverse - * @returns A reversed version of the array - */ - return array.reverse(); - }; - - public rotate = (n: number): Function => { - /** - * Returns a partially applied function that rotates an array by n. - * - */ - - return (array: T[]): T[] => { - return array.slice(n, array.length).concat(array.slice(0, n)); - }; - }; - - public repeat = (n: number): Function => { - /** - * Returns a partially applied function that repeats each element of an array n times. - * - */ - return (array: T[]): T[] => { - return array.flatMap((x) => Array(n).fill(x)); - }; - }; - - public repeatOdd = (n: number): Function => { - /** - * Returns a partially applied function that repeats each even element of an array n times. - * - */ - return (array: T[]): T[] => { - return array.flatMap((x, i) => (i % 2 === 0 ? Array(n).fill(x) : x)); - }; - }; - - public repeatEven = (n: number): Function => { - /** - * Returns a partially applied function that repeats each even element of an array n times. - * - */ - return (array: T[]): T[] => { - return array.flatMap((x, i) => (i % 2 !== 0 ? Array(n).fill(x) : x)); - }; - }; - - public palindrome = (array: T[]): T[] => { - /** - * Returns a palindrome of an array. - * @param array - The array to palindrome - * @returns A palindrome of the array - */ - return array.concat(array.slice(0, array.length - 1).reverse()); - }; - - // ============================================================= - // Oscilloscope Configuration - // ============================================================= - - public scope = (config: OscilloscopeConfig): void => { - /** - * Configures the oscilloscope. - * @param config - The configuration object - */ - this.app.osc = { - ...this.app.osc, - ...config, - }; - }; - - // ============================================================= - // Resolution - // ============================================================= - - public gif = (options: any) => { - /** - * Displays a GIF on the webpage with customizable options including rotation and timed fade-out. - * @param {Object} options - The configuration object for displaying the GIF. - * @param {string} options.url - The URL of the GIF to display. - * @param {number} [options.posX=0] - The X-coordinate to place the GIF at. - * @param {number} [options.posY=0] - The Y-coordinate to place the GIF at. - * @param {number} [options.opacity=1] - The initial opacity level of the GIF. - * @param {string} [options.size='auto'] - The size of the GIF (can be 'cover', 'contain', or specific dimensions). - * @param {boolean} [options.center=false] - Whether to center the GIF in the window. - * @param {number} [options.rotation=0] - The rotation angle of the GIF in degrees. - * @param {string} [options.filter='none'] - The CSS filter function to apply for color alterations. - * @param {number} [options.duration=10] - The total duration the GIF is displayed, in pulses. - */ - const { - url, - posX = 0, - posY = 0, - opacity = 1, - size = "auto", - center = false, - rotation = 0, - filter = "none", - dur = 1, - } = options; - - let real_duration = - dur * this.app.clock.pulse_duration * this.app.clock.ppqn; - let fadeOutDuration = real_duration * 0.1; - let visibilityDuration = real_duration - fadeOutDuration; - const gifElement = document.createElement("img"); - gifElement.src = url; - gifElement.style.position = "fixed"; - gifElement.style.left = center ? "50%" : `${posX}px`; - gifElement.style.top = center ? "50%" : `${posY}px`; - gifElement.style.opacity = `${opacity}`; - gifElement.style.zIndex = "-1"; - if (size !== "auto") { - gifElement.style.width = size; - gifElement.style.height = size; - } - const transformRules = [`rotate(${rotation}deg)`]; - if (center) { - transformRules.unshift("translate(-50%, -50%)"); - } - gifElement.style.transform = transformRules.join(" "); - gifElement.style.filter = filter; - gifElement.style.transition = `opacity ${fadeOutDuration}s ease`; - document.body.appendChild(gifElement); - - // Start the fade-out at the end of the visibility duration - setTimeout(() => { - gifElement.style.opacity = "0"; - }, visibilityDuration * 1000); - - // Remove the GIF from the DOM after the fade-out duration - setTimeout(() => { - if (document.body.contains(gifElement)) { - document.body.removeChild(gifElement); - } - }, real_duration * 1000); - }; - - // ============================================================= - // Canvas Functions - // ============================================================= - - public pulseLocation = (): number => { - /** - * Returns the current pulse location in the current bar. - * @returns The current pulse location in the current bar - */ - return ((this.epulse() / this.pulsesForBar()) * this.w()) % this.w() - } - - public clear = (): boolean => { - /** - * Clears the canvas after a given timeout. - * @param timeout - The timeout in seconds - */ - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - const ctx = canvas.getContext("2d")!; - ctx.clearRect(0, 0, canvas.width, canvas.height); - return true; - } - - public w = (): number => { - /** - * Returns the width of the canvas. - * @returns The width of the canvas - */ - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - return canvas.clientWidth; - } - - public h = (): number => { - /** - * Returns the height of the canvas. - * @returns The height of the canvas - */ - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - return canvas.clientHeight; - } - - public hc = (): number => { - /** - * Returns the center y-coordinate of the canvas. - * @returns The center y-coordinate of the canvas - */ - return this.h() / 2; - } - - public wc = (): number => { - /** - * Returns the center x-coordinate of the canvas. - * @returns The center x-coordinate of the canvas - */ - return this.w() / 2; - } - - public background = (color: string | number, ...gb: number[]): boolean => { - /** - * Set background color of the canvas. - * @param color - The color to set. String or 3 numbers representing RGB values. - */ - drawBackground(this.app.interface.drawings as HTMLCanvasElement, color, ...gb); - return true; - } - bg = this.background; - - public linearGradient = (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]): CanvasGradient => { - /** - * Set linear gradient on the canvas. - * @param x1 - The x-coordinate of the start point - * @param y1 - The y-coordinate of the start point - * @param x2 - The x-coordinate of the end point - * @param y2 - The y-coordinate of the end point - * @param stops - The stops to set. Pairs of numbers representing the position and color of the stop. - */ - return createLinearGradient(this.app.interface.drawings as HTMLCanvasElement, x1, y1, x2, y2, ...stops); - } - - public radialGradient = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number, ...stops: (number | string)[]) => { - /** - * Set radial gradient on the canvas. - * @param x1 - The x-coordinate of the start circle - * @param y1 - The y-coordinate of the start circle - * @param r1 - The radius of the start circle - * @param x2 - The x-coordinate of the end circle - * @param y2 - The y-coordinate of the end circle - * @param r2 - The radius of the end circle - * @param stops - The stops to set. Pairs of numbers representing the position and color of the stop. - */ - return createRadialGradient(this.app.interface.drawings as HTMLCanvasElement, x1, y1, r1, x2, y2, r2, ...stops); - } - - public conicGradient = (x: number, y: number, angle: number, ...stops: (number | string)[]) => { - /** - * Set conic gradient on the canvas. - * @param x - The x-coordinate of the center of the gradient - * @param y - The y-coordinate of the center of the gradient - * @param angle - The angle of the gradient, in radians - * @param stops - The stops to set. Pairs of numbers representing the position and color of the stop. - */ - return createConicGradient(this.app.interface.drawings as HTMLCanvasElement, x, y, angle, ...stops); - } - - public draw = (func: Function): boolean => { - /** - * Draws on the canvas. - * @param func - The function to execute - */ - if (typeof func === "string") { - this.drawText(func); - } else { - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - const ctx = canvas.getContext("2d")!; - func(ctx); - } - return true; - } - - public balloid = ( - curves: number | ShapeObject = 6, - radius: number = this.hc() / 2, - curve: number = 1.5, - fillStyle: string = "white", - secondary: string = "black", - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof curves === "object") { - fillStyle = curves.fillStyle || "white"; - x = curves.x || this.wc(); - y = curves.y || this.hc(); - curve = curves.curve || 1.5; - radius = curves.radius || this.hc() / 2; - curves = curves.curves || 6; - } - drawBalloid(this.app.interface.drawings as HTMLCanvasElement, curves, radius, curve, fillStyle, secondary, x, y); - return true; - }; - - public equilateral = ( - radius: number | ShapeObject = this.hc() / 3, - fillStyle: string = "white", - rotation: number = 0, - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof radius === "object") { - fillStyle = radius.fillStyle || "white"; - x = radius.x || this.wc(); - y = radius.y || this.hc(); - rotation = radius.rotation || 0; - radius = radius.radius || this.hc() / 3; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawEquilateral(canvas, radius, fillStyle, rotation, x, y); - return true; - } - - public triangular = ( - width: number | ShapeObject = this.hc() / 3, - height: number = this.hc() / 3, - fillStyle: string = "white", - rotation: number = 0, - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof width === "object") { - fillStyle = width.fillStyle || "white"; - x = width.x || this.wc(); - y = width.y || this.hc(); - rotation = width.rotation || 0; - height = width.height || this.hc() / 3; - width = width.width || this.hc() / 3; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawTriangular(canvas, width, height, fillStyle, rotation, x, y); - return true; - } - pointy = this.triangular; - - public ball = ( - radius: number | ShapeObject = this.hc() / 3, - fillStyle: string = "white", - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof radius === "object") { - fillStyle = radius.fillStyle || "white"; - x = radius.x || this.wc(); - y = radius.y || this.hc(); - radius = radius.radius || this.hc() / 3; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawBall(canvas, radius, fillStyle, x, y); - return true; - } - circle = this.ball; - - public donut = ( - slices: number | ShapeObject = 3, - eaten: number = 0, - radius: number = this.hc() / 3, - hole: number = this.hc() / 12, - fillStyle: string = "white", - secondary: string = "black", - stroke: string = "black", - rotation: number = 0, - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof slices === "object") { - fillStyle = slices.fillStyle || "white"; - x = slices.x || this.wc(); - y = slices.y || this.hc(); - rotation = slices.rotation || 0; - radius = slices.radius || this.hc() / 3; - eaten = slices.eaten || 0; - hole = slices.hole || this.hc() / 12; - secondary = slices.secondary || "black"; - stroke = slices.stroke || "black"; - slices = slices.slices || 3; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawDonut(canvas, slices, eaten, radius, hole, fillStyle, secondary, stroke, rotation, x, y); - return true; - }; - - - public pie = ( - slices: number | ShapeObject = 3, - eaten: number = 0, - radius: number = this.hc() / 3, - fillStyle: string = "white", - secondary: string = "black", - stroke: string = "black", - rotation: number = 0, - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof slices === "object") { - fillStyle = slices.fillStyle || "white"; - x = slices.x || this.wc(); - y = slices.y || this.hc(); - rotation = slices.rotation || 0; - radius = slices.radius || this.hc() / 3; - secondary = slices.secondary || "black"; - stroke = slices.stroke || "black"; - eaten = slices.eaten || 0; - slices = slices.slices || 3; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawPie(canvas, slices, eaten, radius, fillStyle, secondary, stroke, rotation, x, y); - return true; - }; - - - - public star = ( - points: number | ShapeObject = 5, - radius: number = this.hc() / 3, - fillStyle: string = "white", - rotation: number = 0, - outerRadius: number = radius / 100, - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof points === "object") { - radius = points.radius || this.hc() / 3; - fillStyle = points.fillStyle || "white"; - x = points.x || this.wc(); - y = points.y || this.hc(); - rotation = points.rotation || 0; - outerRadius = points.outerRadius || radius / 100; - points = points.points || 5; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawStar(canvas, points, radius, fillStyle, rotation, outerRadius, x, y); - return true; - }; - - public stroke = ( - width: number | ShapeObject = 1, - strokeStyle: string = "white", - rotation: number = 0, - x1: number = this.wc() - this.wc() / 10, - y1: number = this.hc(), - x2: number = this.wc() + this.wc() / 5, - y2: number = this.hc(), - ): boolean => { - if (typeof width === "object") { - strokeStyle = width.strokeStyle || "white"; - x1 = width.x1 || this.wc() - this.wc() / 10; - y1 = width.y1 || this.hc(); - x2 = width.x2 || this.wc() + this.wc() / 5; - y2 = width.y2 || this.hc(); - rotation = width.rotation || 0; - width = width.width || 1; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawStroke(canvas, width, strokeStyle, rotation, x1, y1, x2, y2); - return true; - }; - - public box = ( - width: number | ShapeObject = this.wc() / 4, - height: number = this.wc() / 4, - fillStyle: string = "white", - rotation: number = 0, - x: number = this.wc() - this.wc() / 8, - y: number = this.hc() - this.hc() / 8, - ): boolean => { - if (typeof width === "object") { - fillStyle = width.fillStyle || "white"; - x = width.x || this.wc() - this.wc() / 4; - y = width.y || this.hc() - this.hc() / 2; - rotation = width.rotation || 0; - height = width.height || this.wc() / 4; - width = width.width || this.wc() / 4; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawStroke(canvas, width, fillStyle, rotation, x, y, width, height); - return true; - } - - public smiley = ( - happiness: number | ShapeObject = 0, - radius: number = this.hc() / 3, - eyeSize: number = 3.0, - fillStyle: string = "yellow", - rotation: number = 0, - x: number = this.wc(), - y: number = this.hc(), - ): boolean => { - if (typeof happiness === "object") { - fillStyle = happiness.fillStyle || "yellow"; - x = happiness.x || this.wc(); - y = happiness.y || this.hc(); - rotation = happiness.rotation || 0; - eyeSize = happiness.eyeSize || 3.0; - radius = happiness.radius || this.hc() / 3; - happiness = happiness.happiness || 0; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawSmiley(canvas, happiness, radius, eyeSize, fillStyle, rotation, x, y); - return true; - } - - drawText = ( - text: string | ShapeObject, - fontSize: number = 24, - rotation: number = 0, - font: string = "Arial", - x: number = this.wc(), - y: number = this.hc(), - fillStyle: string = "white", - filter: string = "none", - ): boolean => { - if (typeof text === "object") { - fillStyle = text.fillStyle || "white"; - x = text.x || this.wc(); - y = text.y || this.hc(); - rotation = text.rotation || 0; - font = text.font || "Arial"; - fontSize = text.fontSize || 24; - filter = text.filter || "none"; - text = text.text || ""; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawText(canvas, text, fontSize, rotation, font, x, y, fillStyle, filter); - return true; - } - - image = ( - url: string | ShapeObject, - width: number = this.wc() / 2, - height: number = this.hc() / 2, - rotation: number = 0, - x: number = this.wc(), - y: number = this.hc(), - filter: string = "none", - ): boolean => { - if (typeof url === "object") { - if (!url.url) return true; - x = url.x || this.wc(); - y = url.y || this.hc(); - rotation = url.rotation || 0; - width = url.width || 100; - height = url.height || 100; - filter = url.filter || "none"; - url = url.url || ""; - } - const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - drawImage(canvas, url, width, height, rotation, x, y, filter); - return true; - } - - randomChar = (length: number = 1, min: number = 0, max: number = 65536): string => { - return Array.from( - { length }, () => String.fromCodePoint(Math.floor(Math.random() * (max - min) + min)) - ).join(''); - } - - randomFromRange = (min: number, max: number): string => { - const codePoint = Math.floor(Math.random() * (max - min) + min); - return String.fromCodePoint(codePoint); - }; - - emoji = (n: number = 1): string => { - return this.randomChar(n, 0x1f600, 0x1f64f); - }; - - food = (n: number = 1): string => { - return this.randomChar(n, 0x1f32d, 0x1f37f); - }; - - animals = (n: number = 1): string => { - return this.randomChar(n, 0x1f400, 0x1f4d3); - }; - - expressions = (n: number = 1): string => { - return this.randomChar(n, 0x1f910, 0x1f92f); - }; - - // ============================================================= - // OSC Functions - // ============================================================= - - public osc = (address: string, port: number, ...args: any[]): void => { - sendToServer({ - address: address, - port: port, - args: args, - timetag: Math.round(Date.now() + (this.app.clock.nudge - this.app.clock.deviation)), - } as OSCMessage); - }; - - public getOSC = (address?: string): any[] => { - /** - * Give access to incoming OSC messages. If no address is specified, returns the raw oscMessages array. If an address is specified, returns only the messages who contain the address and filter the address itself. - */ - if (address) { - let messages = oscMessages.filter((msg) => msg.address === address); - messages = messages.map((msg) => msg.data); - return messages; - } else { - return oscMessages; - } - }; - - // ============================================================= - // Transport functions - // ============================================================= - - public tempo = (n?: number): number => { - /** - * Sets or returns the current bpm. - * - * @param bpm - [optional] The bpm to set - * @returns The current bpm - */ - if (n === undefined) return this.app.clock.bpm; - - if (n < 1 || n > 500) { - this.app.clock.bpm = n; - } - return n; - }; - // tempo = this.bpm; - - public bpb = (n?: number): number => { - /** - * Sets or returns the number of beats per bar. - * - * @param bpb - [optional] The number of beats per bar to set - * @returns The current bpb - */ - if (n === undefined) return this.app.clock.time_signature[0]; - - if (n < 1) console.log(`Setting bpb to ${n}`); - this.app.clock.time_signature[0] = n; - return n; - }; - - public ppqn = (n?: number) => { - /** - * Sets or returns the number of pulses per quarter note. - */ - if (n === undefined) return this.app.clock.ppqn; - - if (n < 1) console.log(`Setting ppqn to ${n}`); - this.app.clock.ppqn = n; - return n; - }; - - public time_signature = (numerator: number, denominator: number): void => { - /** - * Sets the time signature. - * - * @param numerator - The numerator of the time signature - * @param denominator - The denominator of the time signature - * @returns The current time signature - */ - this.app.clock.time_signature = [numerator, denominator]; - }; - - public cue = (functionName: string | Function): void => { - functionName = typeof functionName === "function" ? functionName.name : functionName; - this.cueTimes[functionName] = this.app.clock.pulses_since_origin; - }; - - public theme = (color_scheme: string): void => { - this.app.readTheme(color_scheme); - } - - public themeName = (): string => { - return this.app.currentThemeName; - } - - public randomTheme = (): void => { - let theme_names = this.getThemes(); - let selected_theme = theme_names[Math.floor(Math.random() * theme_names.length)]; - this.app.readTheme(selected_theme); - } - - public nextTheme = (): void => { - let theme_names = this.getThemes(); - let current_theme = this.app.api.themeName(); - let current_theme_idx = theme_names.indexOf(current_theme); - let next_theme_idx = (current_theme_idx + 1) % theme_names.length; - let next_theme = theme_names[next_theme_idx]; - this.app.readTheme(next_theme); - this.app.api.log(next_theme); - } - - public getThemes = (): string[] => { - return Object.keys(colorschemes); - } - -} diff --git a/src/API/API.ts b/src/API/API.ts new file mode 100644 index 0000000..3e351b3 --- /dev/null +++ b/src/API/API.ts @@ -0,0 +1,652 @@ +import * as Transport from './Transport'; +import * as Mouse from './Mouse'; +import * as Theme from './Theme'; +import * as Canvas from './Canvas'; +import * as Cache from './Cache'; +import * as Script from './Script'; +import * as Drunk from './Drunk'; +import * as Warp from './Warp'; +import * as Mathematics from './Math'; +import * as Ziffers from './Ziffers'; +import * as Filters from './Filters'; +import * as LFO from './LFO'; +import * as Probability from './Probabilities'; +import * as OSC from './OSC'; +import * as Randomness from './Randomness'; +import * as Counter from './Counter'; +import * as Sound from './Sound'; +import * as Console from './Console'; +import { type SoundEvent } from '../Classes/SoundEvent'; +import { type SkipEvent } from '../Classes/SkipEvent'; +import { OscilloscopeConfig } from "../DOM/Visuals/Oscilloscope"; +import { Player } from "../Classes/ZPlayer"; +import { InputOptions } from "../Classes/ZPlayer"; +import { type ShapeObject } from "../DOM/Visuals/CanvasVisuals"; +import { nearScales } from "zifferjs"; +import { MidiConnection } from "../IO/MidiConnection"; +import { evaluateOnce } from "../Evaluator"; +import { DrunkWalk } from "../Utils/Drunk"; +import { Editor } from "../main"; +import { LRUCache } from "lru-cache"; +import { + loadUniverse, + openUniverseModal, +} from "../Editor/FileManagement"; +import { + samples, + initAudioOnFirstClick, + registerSynthSounds, + registerZZFXSounds, + soundMap, + // @ts-ignore +} from "superdough"; +import { Speaker } from "../Extensions/StringExtensions"; +import { getScaleNotes } from "zifferjs"; +import { AbstractEvent, EventOperation } from "../Classes/AbstractEvents"; +import drums from "../tidal-drum-machines.json"; + +export async function loadSamples() { + return Promise.all([ + initAudioOnFirstClick(), + samples("github:tidalcycles/Dirt-Samples/master", undefined, { + tag: "Tidal", + }).then(() => registerSynthSounds()), + registerZZFXSounds(), + samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", { + tag: "Machines", + }), + samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, { + tag: "FoxDot", + }), + samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, { + tag: "Pack", + }), + samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, { + tag: "Amiga", + }), + samples("github:Bubobubobubobubo/Dough-Juj/main", undefined, { + tag: "Juliette", + }), + samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, { + tag: "Amen", + }), + samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, { + tag: "Waveforms", + }), + ]); +} + +export class UserAPI { + /** + * The UserAPI class is the interface between the user's code and the backend. It provides + * access to the AudioContext, to the MIDI Interface, to internal variables, mouse position, + * useful functions, etc... This class is exposed to the user's action and any function + * destined to the user should be placed here. + */ + + public codeExamples: { [key: string]: string } = {}; + private counters: { [key: string]: any } = {}; + private _drunk: DrunkWalk = new DrunkWalk(-100, 100, false); + public randomGen = Math.random; + public currentSeed: string | undefined = undefined; + public localSeeds = new Map(); + public patternCache = new LRUCache({ max: 10000, ttl: 10000 * 60 * 5 }); + public invalidPatterns: { [key: string]: boolean } = {}; + public cueTimes: { [key: string]: number } = {}; + private errorTimeoutID: number = 0; + private printTimeoutID: number = 0; + public MidiConnection: MidiConnection; + public scale_aid: string | number | undefined = undefined; + public hydra: any; + public onceEvaluator: boolean = true; + public forceEvaluator: boolean = false; + + load: samples; + public global: { [key: string]: any }; + time: () => number; + play: () => void; + pause: () => void; + stop: () => void; + silence: () => void; + onMouseMove: (e: MouseEvent) => void; + mouseX: () => number; + mouseY: () => number; + noteX: () => number; + noteY: () => number; + tempo: (n?: number | undefined) => number; + bpb: (n?: number | undefined) => number; + ppqn: (n?: number | undefined) => number; + time_signature: (numerator: number, denominator: number) => void; + theme: (color_scheme: string) => void; + themeName: () => string; + randomTheme: () => void; + nextTheme: () => void; + getThemes: () => string[]; + pulseLocation: () => number; + clear: () => boolean; + w: () => number; + h: () => number; + hc: () => number; + wc: () => number; + background: (color: string | number, ...gb: number[]) => boolean; + linearGradient: (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]) => CanvasGradient; + radialGradient: (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number, ...stops: (number | string)[]) => CanvasGradient; + conicGradient: (x: number, y: number, angle: number, ...stops: (number | string)[]) => CanvasGradient; + draw: (func: Function) => boolean; + balloid: (curves: number | ShapeObject, radius: number, curve: number, fillStyle: string, secondary: string, x: number, y: number) => boolean; + equilateral: (radius: number | ShapeObject, fillStyle: string, rotation: number, x: number, y: number) => boolean; + triangular: (width: number | ShapeObject, height: number, fillStyle: string, rotation: number, x: number, y: number) => boolean; + ball: (radius: number | ShapeObject, fillStyle: string, x: number, y: number) => boolean; + circle: (radius: number | ShapeObject, fillStyle: string, x: number, y: number) => boolean; + donut: (slices: number | ShapeObject, eaten: number, radius: number, hole: number, fillStyle: string, secondary: string, stroke: string, rotation: number, x: number, y: number) => boolean; + pie: (slices: number | ShapeObject, eaten: number, radius: number, fillStyle: string, secondary: string, stroke: string, rotation: number, x: number, y: number) => boolean; + star: (points: number | ShapeObject, radius: number, fillStyle: string, rotation: number, outerRadius: number, x: number, y: number) => boolean; + stroke: (width: number | ShapeObject, strokeStyle: string, rotation: number, x1: number, y1: number, x2: number, y2: number) => boolean; + box: (width: number | ShapeObject, height: number, fillStyle: string, rotation: number, x: number, y: number) => boolean; + smiley: (happiness: number | ShapeObject, radius: number, eyeSize: number, fillStyle: string, rotation: number, x: number, y: number) => boolean; + text: (text: string | ShapeObject, fontSize: number, rotation: number, font: string, x: number, y: number, fillStyle: string, filter: string) => boolean; + image: (url: string | ShapeObject, width: number, height: number, rotation: number, x: number, y: number, filter: string) => boolean; + randomChar: (length: number, min: number, max: number) => string; + randomFromRange: (min: number, max: number) => string; + emoji: (n: number) => string; + food: (n: number) => string; + animals: (n: number) => string; + expressions: (n: number) => string; + generateCacheKey: (...args: any[]) => string; + resetAllFromCache: () => void; + clearPatternCache: () => void; + removePatternFromCache: (id: string) => void; + script: (...args: number[]) => void; + s: (...args: number[]) => void; + delete_script: (script: number) => void; + cs: (script: number) => void; + copy_script: (from: number, to: number) => void; + cps: (from: number, to: number) => void; + copy_universe: (from: string, to: string) => void; + delete_universe: (universe: string) => void; + big_bang: () => void; + drunk: (n?: number | undefined) => number; + drunk_max: (max: number) => void; + drunk_min: (min: number) => void; + drunk_wrap: (wrap: boolean) => void; + warp: (n: number) => void; + beat_warp: (beat: number) => void; + min: (...values: number[]) => number; + max: (...values: number[]) => number; + mean: (...values: number[]) => number; + limit: (value: number, min: number, max: number) => number; + abs: (value: number) => number; + z: (input: string | Generator, options: InputOptions, id: number | string) => Player; + fullseq: (sequence: string, duration: number) => boolean | boolean[]; + seq: (expr: string, duration?: number) => boolean; + beat: (n?: number | number[], nudge?: number) => boolean; + bar: (n?: number | number[], nudge?: number) => boolean; + pulse: (n?: number | number[], nudge?: number) => boolean; + tick: (tick: number | number[], offset?: number) => boolean; + dur: (n: number | number[]) => boolean; + flip: (chunk: number, ratio?: number) => boolean; + flipbar: (chunk?: number) => boolean; + onbar: (bars: number | number[], n?: number) => boolean; + onbeat: (...beat: number[]) => boolean; + oncount: (beats: number | number[], count: number) => boolean; + oneuclid: (pulses: number, length: number, rotate?: number) => boolean; + euclid: (iterator: number, pulses: number, length: number, rotate?: number) => boolean; + ec: any; + rhythm: (div: number, pulses: number, length: number, rotate?: number) => boolean; + ry: any; + nrhythm: (div: number, pulses: number, length: number, rotate?: number) => boolean; + nry: any; + bin: (iterator: number, n: number) => boolean; + binrhythm: (div: number, n: number) => boolean; + bry: any; + line: any; + sine: any; + usine: any; + saw: any; + usaw: any; + triangle: any; + utriangle: any; + square: any; + usquare: any; + noise: any; + unoise: any; + prob: (p: number) => boolean; + toss: () => boolean; + odds: (n: number, beats?: number) => boolean; + never: (beats?: number) => boolean; + almostNever: (beats?: number) => boolean; + rarely: (beats?: number) => boolean; + scarcely: (beats?: number) => boolean; + sometimes: (beats?: number) => boolean; + often: (beats?: number) => boolean; + frequently: (beats?: number) => boolean; + almostAlways: (beats?: number) => boolean; + always: (beats?: number) => boolean; + dice: (sides: number) => number; + osc: (address: string, port: number, ...args: any[]) => void; + getOSC: (address?: string | undefined) => any[]; + gif: (options: any) => void; + scope: (config: OscilloscopeConfig) => void; + randI: any; + rand: any; + seed: any; + localSeededRandom: any; + clearLocalSeed: any; + once: () => boolean; + counter: (name: string | number, limit?: number | undefined, step?: number | undefined) => number; + $: any; + count: any; + i: (n?: number | undefined) => any; + sound: (sound: string | string[] | null | undefined) => SoundEvent | SkipEvent; + snd: any; + log: (message: any) => void; + logOnce: (message: any) => void; + + constructor(public app: Editor) { + this.MidiConnection = new MidiConnection(this, app.settings); + this.global = {}; + this.g = this.global; + this.time = Transport.time(this.app); + this.play = Transport.play(this.app); + this.pause = Transport.pause(this.app); + this.stop = Transport.stop(this.app); + this.silence = Transport.silence(this.app); + this.tempo = Transport.tempo(this.app); + this.bpb = Transport.bpb(this.app); + this.ppqn = Transport.ppqn(this.app); + this.time_signature = Transport.time_signature(this.app); + this.onMouseMove = Mouse.onmousemove(this.app); + this.mouseX = Mouse.mouseX(this.app); + this.mouseY = Mouse.mouseY(this.app); + this.noteX = Mouse.noteX(this.app); + this.noteY = Mouse.noteY(this.app); + this.theme = Theme.theme(this.app); + this.themeName = Theme.themeName(this.app); + this.randomTheme = Theme.randomTheme(this.app); + this.nextTheme = Theme.nextTheme(this.app); + this.getThemes = Theme.getThemes(); + this.pulseLocation = Canvas.pulseLocation(this.app); + this.clear = Canvas.clear(this.app); + this.w = Canvas.w(this.app); + this.h = Canvas.h(this.app); + this.hc = Canvas.hc(this.app); + this.wc = Canvas.wc(this.app); + this.background = Canvas.background(this.app); + this.linearGradient = Canvas.linearGradient(this.app); + this.radialGradient = Canvas.radialGradient(this.app); + this.conicGradient = Canvas.conicGradient(this.app); + this.draw = Canvas.draw(this.app); + this.balloid = Canvas.balloid(this.app); + this.equilateral = Canvas.equilateral(this.app); + this.triangular = Canvas.triangular(this.app); + this.ball = Canvas.ball(this.app); + this.circle = Canvas.circle(this.app); + this.donut = Canvas.donut(this.app); + this.pie = Canvas.pie(this.app); + this.star = Canvas.star(this.app); + this.stroke = Canvas.stroke(this.app); + this.box = Canvas.box(this.app); + this.smiley = Canvas.smiley(this.app); + this.text = Canvas.text(this.app); + this.image = Canvas.image(this.app); + this.randomChar = Canvas.randomChar(); + this.randomFromRange = Canvas.randomFromRange(); + this.emoji = Canvas.emoji(); + this.food = Canvas.food(); + this.animals = Canvas.animals(); + this.expressions = Canvas.expressions(); + this.generateCacheKey = Cache.generateCacheKey(this.app); + this.resetAllFromCache = Cache.resetAllFromCache(this.app); + this.clearPatternCache = Cache.clearPatternCache(this.app); + this.removePatternFromCache = Cache.removePatternFromCache(this.app); + this.script = Script.script(this.app); + this.s = this.script; + this.delete_script = Script.delete_script(this.app); + this.cs = this.delete_script; + this.copy_script = Script.copy_script(this.app); + this.cps = this.copy_script; + this.copy_universe = Script.copy_universe(this.app); + this.delete_universe = Script.delete_universe(this.app); + this.big_bang = Script.big_bang(this.app); + this.drunk = Drunk.drunk(this.app); + this.drunk_max = Drunk.drunk_max(this.app); + this.drunk_min = Drunk.drunk_min(this.app); + this.drunk_wrap = Drunk.drunk_wrap(this.app); + this.warp = Warp.warp(this.app); + this.beat_warp = Warp.beat_warp(this.app); + this.min = Mathematics.min(this.app); + this.max = Mathematics.max(this.app); + this.mean = Mathematics.mean(this.app); + this.limit = Mathematics.limit(this.app); + this.abs = Mathematics.abs(this.app); + this.z = Ziffers.z(this.app); + Object.assign(this, Ziffers.generateZFunctions(this.app)); + this.fullseq = Filters.fullseq(); + this.seq = Filters.seq(this.app); + this.beat = Filters.beat(this.app); + this.bar = Filters.bar(this.app); + this.pulse = Filters.pulse(this.app); + this.tick = Filters.tick(this.app); + this.dur = Filters.dur(this.app); + this.flip = Filters.flip(this.app); + this.flipbar = Filters.flipbar(this.app); + this.onbar = Filters.onbar(this.app); + this.onbeat = Filters.onbeat(this.app); + this.oncount = Filters.oncount(this.app); + this.oneuclid = Filters.oneuclid(this.app); + this.euclid = Filters.euclid(this.app); + this.ec = this.euclid; + this.rhythm = Filters.rhythm(this.app); + this.ry = this.rhythm; + this.nrhythm = Filters.nrhythm(this.app); + this.nry = this.nrhythm; + this.bin = Filters.bin(); + this.binrhythm = Filters.binrhythm(this.app); + this.bry = this.binrhythm; + this.line = LFO.line(this.app); + this.sine = LFO.sine(this.app); + this.usine = LFO.usine(this.app); + this.saw = LFO.saw(this.app); + this.usaw = LFO.usaw(this.app); + this.triangle = LFO.triangle(this.app); + this.utriangle = LFO.utriangle(this.app); + this.square = LFO.square(this.app); + this.usquare = LFO.usquare(this.app); + this.noise = LFO.noise(this.app); + this.unoise = LFO.unoise(this.app); + this.prob = Probability.prob(this.app); + this.toss = Probability.toss(this.app); + this.odds = Probability.odds(this.app); + this.never = Probability.never(this.app); + this.almostNever = Probability.almostNever(this.app); + this.rarely = Probability.rarely(this.app); + this.scarcely = Probability.scarcely(this.app); + this.sometimes = Probability.sometimes(this.app); + this.often = Probability.often(this.app); + this.frequently = Probability.frequently(this.app); + this.almostAlways = Probability.almostAlways(this.app); + this.always = Probability.always(this.app); + this.dice = Probability.dice(this.app); + this.osc = OSC.osc(this.app); + this.getOSC = OSC.getOSC(this.app); + this.gif = Canvas.gif(this.app); + this.scope = Canvas.scope(this.app); + this.randI = Randomness.randI(this.app); + this.rand = Randomness.rand(this.app); + this.seed = Randomness.seed(this.app); + this.localSeededRandom = Randomness.localSeededRandom(this.app); + this.clearLocalSeed = Randomness.clearLocalSeed(this.app); + this.once = Counter.once(this.app); + this.counter = Counter.counter(this.app); + this.$ = this.counter; + this.count = this.counter; + this.i = Counter.i(this.app); + this.sound = Sound.sound(this.app); + this.snd = this.sound; // Alias + this.speak = Sound.speak(this.app); + this.log = Console.log(this.app); + this.logOnce = Console.logOnce(this.app); + + } + + public g: any; + + _loadUniverseFromInterface = (universe: string) => { + this.app.selected_universe = universe.trim(); + this.app.settings.selected_universe = universe.trim(); + loadUniverse(this.app, universe as string); + openUniverseModal(); + }; + + _deleteUniverseFromInterface = (universe: string) => { + delete this.app.universes[universe]; + if (this.app.settings.selected_universe === universe) { + this.app.settings.selected_universe = "Welcome"; + this.app.selected_universe = "Welcome"; + } + this.app.settings.saveApplicationToLocalStorage( + this.app.universes, + this.app.settings, + ); + this.app.updateKnownUniversesView(); + }; + + _playDocExample = (code?: string) => { + /** + * Play an example from the documentation. The example is going + * to be stored in the example buffer belonging to the universe. + * This buffer is going to be cleaned everytime the user press + * pause or leaves the documentation window. + * + * @param code - The code example to play (identifier) + */ + let current_universe = this.app.universes[this.app.selected_universe]; + this.app.exampleIsPlaying = true; + if (!current_universe.example) { + current_universe.example = { + candidate: "", + committed: "", + evaluations: 0, + }; + current_universe.example.candidate! = code + ? code + : (this.app.selectedExample as string); + } else { + current_universe.example.candidate! = code + ? code + : (this.app.selectedExample as string); + } + this.clearPatternCache(); + this.stop(); + this.play(); + }; + + _stopDocExample = () => { + let current_universe = this.app.universes[this.app.selected_universe]; + if (current_universe?.example !== undefined) { + this.app.exampleIsPlaying = false; + current_universe.example.candidate! = ""; + current_universe.example.committed! = ""; + } + this.clearPatternCache(); + this.stop(); + }; + + _playDocExampleOnce = (code?: string) => { + let current_universe = this.app.universes[this.app.selected_universe]; + if (current_universe?.example !== undefined) { + current_universe.example.candidate! = ""; + current_universe.example.committed! = ""; + } + this.clearPatternCache(); + this.stop(); + this.play(); + this.app.exampleIsPlaying = true; + evaluateOnce(this.app, code as string); + }; + + _all_samples = (): object => { + return soundMap.get(); + }; + + _reportError = (error: any): void => { + const extractLineAndColumn = (error: Error) => { + const stackLines = error.stack?.split("\n"); + if (stackLines) { + for (const line of stackLines) { + if (line.includes("")) { + const match = line.match(/:(\d+):(\d+)/); + if (match) + return { + line: parseInt(match[1], 10), + column: parseInt(match[2], 10), + }; + } + } + } + return { line: null, column: null }; + }; + + const { line, column } = extractLineAndColumn(error); + const errorMessage = + line && column + ? `${error.message} (Line: ${line - 2}, Column: ${column})` + : error.message; + + clearTimeout(this.errorTimeoutID); + clearTimeout(this.printTimeoutID); + this.app.interface.error_line.innerHTML = errorMessage; + this.app.interface.error_line.style.color = "red"; + this.app.interface.error_line.classList.remove("hidden"); + // @ts-ignore + this.errorTimeoutID = setTimeout( + () => this.app.interface.error_line.classList.add("hidden"), + 2000, + ); + }; + + _logMessage = (message: any, error: boolean = false): void => { + console.log(message); + clearTimeout(this.printTimeoutID); + clearTimeout(this.errorTimeoutID); + this.app.interface.error_line.innerHTML = message as string; + this.app.interface.error_line.style.color = error ? "red" : "white"; + this.app.interface.error_line.classList.remove("hidden"); + // @ts-ignore + this.printTimeoutID = setTimeout( + () => this.app.interface.error_line.classList.add("hidden"), + 4000, + ); + }; + + + + // ============================================================= + // Quantification functions + // ============================================================= + + public quantize = (value: number, quantization: number[]): number => { + /** + * Returns the closest value in an array to a given value. + * + * @param value - The value to quantize + * @param quantization - The array of values to quantize to + * @returns The closest value in the array to the given value + */ + if (quantization.length === 0) { + return value; + } + let closest = quantization[0]; + quantization.forEach((q) => { + if (Math.abs(q - value) < Math.abs(closest - value)) { + closest = q; + } + }); + return closest; + }; + quant = this.quantize; + + public clamp = (value: number, min: number, max: number): number => { + /** + * Returns a value clamped between min and max. + * + * @param value - The value to clamp + * @param min - The minimum value of the clamped value + * @param max - The maximum value of the clamped value + * @returns A value clamped between min and max + */ + return Math.min(Math.max(value, min), max); + }; + cmp = this.clamp; + + // ============================================================= + // Time markers + // ============================================================= + + cbar = (): number => { + /** + * Returns the current bar number + * + * @returns The current bar number + */ + return this.app.clock.time_position.bar + 1; + }; + + ctick = (): number => { + /** + * Returns the current tick number + * + * @returns The current tick number + */ + return this.app.clock.tick + 1; + }; + + cpulse = (): number => { + /** + * Returns the current pulse number + * + * @returns The current pulse number + */ + return this.app.clock.time_position.pulse + 1; + }; + + cbeat = (): number => { + /** + * Returns the current beat number + * + * @returns The current beat number + */ + return this.app.clock.time_position.beat + 1; + }; + + ebeat = (): number => { + /** + * Returns the current beat number since the origin of time + */ + return this.app.clock.beats_since_origin + 1; + }; + + epulse = (): number => { + /** + * Returns the current number of pulses elapsed since origin of time + */ + return this.app.clock.pulses_since_origin + 1; + }; + + nominator = (): number => { + /** + * Returns the current nominator of the time signature + */ + return this.app.clock.time_signature[0]; + }; + + meter = (): number => { + /** + * Returns the current meter (denominator of the time signature) + */ + return this.app.clock.time_signature[1]; + }; + + denominator = this.meter; + + pulsesForBar = (): number => { + /** + * Returns the number of pulses in a given bar + */ + return (this.tempo() * this.ppqn() * this.nominator()) / 60; + } + + // ============================================================= + // Fill + // ============================================================= + + public fill = (): boolean => this.app.fill; + + scale = getScaleNotes; + nearScales = nearScales; + + public cue = (functionName: string | Function): void => { + functionName = typeof functionName === "function" ? functionName.name : functionName; + this.cueTimes[functionName] = this.app.clock.pulses_since_origin; + }; +} \ No newline at end of file diff --git a/src/API/Cache.ts b/src/API/Cache.ts new file mode 100644 index 0000000..232adbb --- /dev/null +++ b/src/API/Cache.ts @@ -0,0 +1,60 @@ +import { isGenerator, isGeneratorFunction, maybeToNumber } from "../Utils/Generic"; +import { type Player } from "../Classes/ZPlayer"; + + +export const generateCacheKey = (app: any) => (...args: any[]): string => { + return args.map((arg) => JSON.stringify(arg)).join(","); +}; + +export const resetAllFromCache = (app: any) => (): void => { + app.patternCache.forEach((player: Player) => player.reset()); +}; + +export const clearPatternCache = (app: any) => (): void => { + app.patternCache.clear(); +}; + +export const removePatternFromCache = (app: any) => (id: string): void => { + app.patternCache.delete(id); +}; + + +export const cache = (app: any) => (key: string, value: any) => { + if (value !== undefined) { + if (isGenerator(value)) { + if (app.patternCache.has(key)) { + const cachedValue = (app.patternCache.get(key) as Generator).next().value; + if (cachedValue !== 0 && !cachedValue) { + const generator = value as unknown as Generator; + app.patternCache.set(key, generator); + return maybeToNumber(generator.next().value); + } + return maybeToNumber(cachedValue); + } else { + const generator = value as unknown as Generator; + app.patternCache.set(key, generator); + return maybeToNumber(generator.next().value); + } + } else if (isGeneratorFunction(value)) { + if (app.patternCache.has(key)) { + const cachedValue = (app.patternCache.get(key) as Generator).next().value; + if (cachedValue || cachedValue === 0 || cachedValue === 0n) { + return maybeToNumber(cachedValue); + } else { + const generator = value(); + app.patternCache.set(key, generator); + return maybeToNumber(generator.next().value); + } + } else { + const generator = value(); + app.patternCache.set(key, generator); + return maybeToNumber(generator.next().value); + } + } else { + app.patternCache.set(key, value); + return maybeToNumber(value); + } + } else { + return maybeToNumber(app.patternCache.get(key)); + } +}; \ No newline at end of file diff --git a/src/API/Canvas.ts b/src/API/Canvas.ts new file mode 100644 index 0000000..5d7ba5a --- /dev/null +++ b/src/API/Canvas.ts @@ -0,0 +1,414 @@ +import { OscilloscopeConfig } from "../DOM/Visuals/Oscilloscope"; +import { ShapeObject, createConicGradient, createLinearGradient, createRadialGradient, drawBackground, drawBox, drawBall, drawBalloid, drawDonut, drawEquilateral, drawImage, drawPie, drawSmiley, drawStar, drawStroke, drawText, drawTriangular } from "../DOM/Visuals/CanvasVisuals"; +import { Editor } from "../main"; + +export const w = (app: Editor) => (): number => { + const canvas: HTMLCanvasElement = app.interface.drawings as HTMLCanvasElement; + return canvas.clientWidth; +}; + +export const pulseLocation = (app: Editor) => (): number => { + return ((app.api.epulse() / app.api.pulsesForBar()) * w(app)()) % w(app)(); +}; + +export const clear = (app: Editor) => (): boolean => { + const canvas: HTMLCanvasElement = app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.clearRect(0, 0, canvas.width, canvas.height); + return true; +}; + + +export const h = (app: Editor) => (): number => { + const canvas: HTMLCanvasElement = app.interface.drawings as HTMLCanvasElement; + return canvas.clientHeight; +}; + +export const hc = (app: Editor) => (): number => { + return h(app)() / 2; +}; + +export const wc = (app: Editor) => (): number => { + return w(app)() / 2; +}; + +export const background = (app: Editor) => (color: string | number, ...gb: number[]): boolean => { + drawBackground(app.interface.drawings as HTMLCanvasElement, color, ...gb); + return true; +}; +export const bg = background; + +export const linearGradient = (app: Editor) => (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]): CanvasGradient => { + return createLinearGradient(app.interface.drawings as HTMLCanvasElement, x1, y1, x2, y2, ...stops); +}; + +export const radialGradient = (app: Editor) => (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number, ...stops: (number | string)[]) => { + return createRadialGradient(app.interface.drawings as HTMLCanvasElement, x1, y1, r1, x2, y2, r2, ...stops); +}; + +export const conicGradient = (app: Editor) => (x: number, y: number, angle: number, ...stops: (number | string)[]) => { + return createConicGradient(app.interface.drawings as HTMLCanvasElement, x, y, angle, ...stops); +}; + +export const draw = (app: Editor) => (func: Function): boolean => { + if (typeof func === "string") { + drawText(app.interface.drawings as HTMLCanvasElement, func, 24, 0, "Arial", wc(app)(), hc(app)(), "white", "none"); + } else { + const canvas: HTMLCanvasElement = app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + func(ctx); + } + return true; +}; + +// Additional drawing and utility functions in canvas.ts +export const balloid = (app: Editor) => ( + curves: number | ShapeObject = 6, + radius: number = hc(app)() / 2, + curve: number = 1.5, + fillStyle: string = "white", + secondary: string = "black", + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof curves === "object") { + fillStyle = curves.fillStyle || "white"; + x = curves.x || wc(app)(); + y = curves.y || hc(app)(); + curve = curves.curve || 1.5; + radius = curves.radius || hc(app)() / 2; + curves = curves.curves || 6; + } + drawBalloid(app.interface.drawings as HTMLCanvasElement, curves, radius, curve, fillStyle, secondary, x, y); + return true; +}; + +export const equilateral = (app: Editor) => ( + radius: number | ShapeObject = hc(app)() / 3, + fillStyle: string = "white", + rotation: number = 0, + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof radius === "object") { + fillStyle = radius.fillStyle || "white"; + x = radius.x || wc(app)(); + y = radius.y || hc(app)(); + rotation = radius.rotation || 0; + radius = radius.radius || hc(app)() / 3; + } + drawEquilateral(app.interface.drawings as HTMLCanvasElement, radius, fillStyle, rotation, x, y); + return true; +}; + +export const triangular = (app: Editor) => ( + width: number | ShapeObject = hc(app)() / 3, + height: number = hc(app)() / 3, + fillStyle: string = "white", + rotation: number = 0, + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof width === "object") { + fillStyle = width.fillStyle || "white"; + x = width.x || wc(app)(); + y = width.y || hc(app)(); + rotation = width.rotation || 0; + height = width.height || hc(app)() / 3; + width = width.width || hc(app)() / 3; + } + drawTriangular(app.interface.drawings as HTMLCanvasElement, width, height, fillStyle, rotation, x, y); + return true; +}; +export const pointy = triangular; + +export const ball = (app: Editor) => ( + radius: number | ShapeObject = hc(app)() / 3, + fillStyle: string = "white", + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof radius === "object") { + fillStyle = radius.fillStyle || "white"; + x = radius.x || wc(app)(); + y = radius.y || hc(app)(); + radius = radius.radius || hc(app)() / 3; + } + drawBall(app.interface.drawings as HTMLCanvasElement, radius, fillStyle, x, y); + return true; +}; +export const circle = ball; + +export const donut = (app: Editor) => ( + slices: number | ShapeObject = 3, + eaten: number = 0, + radius: number = hc(app)() / 3, + hole: number = hc(app)() / 12, + fillStyle: string = "white", + secondary: string = "black", + stroke: string = "black", + rotation: number = 0, + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof slices === "object") { + fillStyle = slices.fillStyle || "white"; + x = slices.x || wc(app)(); + y = slices.y || hc(app)(); + rotation = slices.rotation || 0; + radius = slices.radius || hc(app)() / 3; + eaten = slices.eaten || 0; + hole = slices.hole || hc(app)() / 12; + secondary = slices.secondary || "black"; + stroke = slices.stroke || "black"; + slices = slices.slices || 3; + } + drawDonut(app.interface.drawings as HTMLCanvasElement, slices, eaten, radius, hole, fillStyle, secondary, stroke, rotation, x, y); + return true; +}; + +export const pie = (app: Editor) => ( + slices: number | ShapeObject = 3, + eaten: number = 0, + radius: number = hc(app)() / 3, + fillStyle: string = "white", + secondary: string = "black", + stroke: string = "black", + rotation: number = 0, + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof slices === "object") { + fillStyle = slices.fillStyle || "white"; + x = slices.x || wc(app)(); + y = slices.y || hc(app)(); + rotation = slices.rotation || 0; + radius = slices.radius || hc(app)() / 3; + secondary = slices.secondary || "black"; + stroke = slices.stroke || "black"; + eaten = slices.eaten || 0; + slices = slices.slices || 3; + } + drawPie(app.interface.drawings as HTMLCanvasElement, slices, eaten, radius, fillStyle, secondary, stroke, rotation, x, y); + return true; +}; + +export const star = (app: Editor) => ( + points: number | ShapeObject = 5, + radius: number = hc(app)() / 3, + fillStyle: string = "white", + rotation: number = 0, + outerRadius: number = radius / 100, + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof points === "object") { + radius = points.radius || hc(app)() / 3; + fillStyle = points.fillStyle || "white"; + x = points.x || wc(app)(); + y = points.y || hc(app)(); + rotation = points.rotation || 0; + outerRadius = points.outerRadius || radius / 100; + points = points.points || 5; + } + drawStar(app.interface.drawings as HTMLCanvasElement, points, radius, fillStyle, rotation, outerRadius, x, y); + return true; +}; + +export const stroke = (app: Editor) => ( + width: number | ShapeObject = 1, + strokeStyle: string = "white", + rotation: number = 0, + x1: number = wc(app)() - wc(app)() / 10, + y1: number = hc(app)(), + x2: number = wc(app)() + wc(app)() / 5, + y2: number = hc(app)(), +): boolean => { + if (typeof width === "object") { + strokeStyle = width.strokeStyle || "white"; + x1 = width.x1 || wc(app)() - wc(app)() / 10; + y1 = width.y1 || hc(app)(); + x2 = width.x2 || wc(app)() + wc(app)() / 5; + y2 = width.y2 || hc(app)(); + rotation = width.rotation || 0; + width = width.width || 1; + } + drawStroke(app.interface.drawings as HTMLCanvasElement, width, strokeStyle, rotation, x1, y1, x2, y2); + return true; +}; + +export const box = (app: Editor) => ( + width: number | ShapeObject = wc(app)() / 4, + height: number = wc(app)() / 4, + fillStyle: string = "white", + rotation: number = 0, + x: number = wc(app)() - wc(app)() / 8, + y: number = hc(app)() - hc(app)() / 8, +): boolean => { + if (typeof width === "object") { + fillStyle = width.fillStyle || "white"; + x = width.x || wc(app)() - wc(app)() / 4; + y = width.y || hc(app)() - hc(app)() / 2; + rotation = width.rotation || 0; + height = width.height || wc(app)() / 4; + width = width.width || wc(app)() / 4; + } + drawBox(app.interface.drawings as HTMLCanvasElement, width, height, fillStyle, rotation, x, y); + return true; +}; + +export const smiley = (app: Editor) => ( + happiness: number | ShapeObject = 0, + radius: number = hc(app)() / 3, + eyeSize: number = 3.0, + fillStyle: string = "yellow", + rotation: number = 0, + x: number = wc(app)(), + y: number = hc(app)(), +): boolean => { + if (typeof happiness === "object") { + fillStyle = happiness.fillStyle || "yellow"; + x = happiness.x || wc(app)(); + y = happiness.y || hc(app)(); + rotation = happiness.rotation || 0; + eyeSize = happiness.eyeSize || 3.0; + radius = happiness.radius || hc(app)() / 3; + happiness = happiness.happiness || 0; + } + drawSmiley(app.interface.drawings as HTMLCanvasElement, happiness, radius, eyeSize, fillStyle, rotation, x, y); + return true; +}; + +export const text = (app: Editor) => ( + text: string | ShapeObject, + fontSize: number = 24, + rotation: number = 0, + font: string = "Arial", + x: number = wc(app)(), + y: number = hc(app)(), + fillStyle: string = "white", + filter: string = "none", +): boolean => { + if (typeof text === "object") { + fillStyle = text.fillStyle || "white"; + x = text.x || wc(app)(); + y = text.y || hc(app)(); + rotation = text.rotation || 0; + font = text.font || "Arial"; + fontSize = text.fontSize || 24; + filter = text.filter || "none"; + text = text.text || ""; + } + drawText(app.interface.drawings as HTMLCanvasElement, text, fontSize, rotation, font, x, y, fillStyle, filter); + return true; +}; + +export const image = (app: Editor) => ( + url: string | ShapeObject, + width: number = wc(app)() / 2, + height: number = hc(app)() / 2, + rotation: number = 0, + x: number = wc(app)(), + y: number = hc(app)(), + filter: string = "none", +): boolean => { + if (typeof url === "object") { + if (!url.url) return true; + x = url.x || wc(app)(); + y = url.y || hc(app)(); + rotation = url.rotation || 0; + width = url.width || 100; + height = url.height || 100; + filter = url.filter || "none"; + url = url.url || ""; + } + drawImage(app.interface.drawings as HTMLCanvasElement, url, width, height, rotation, x, y, filter); + return true; +}; + +export const randomChar = () => (length: number = 1, min: number = 0, max: number = 65536): string => { + return Array.from( + { length }, () => String.fromCodePoint(Math.floor(Math.random() * (max - min) + min)) + ).join(''); +}; + +export const randomFromRange = () => (min: number, max: number): string => { + const codePoint = Math.floor(Math.random() * (max - min) + min); + return String.fromCodePoint(codePoint); +}; + +export const emoji = () => (n: number = 1): string => { + return randomChar()(n, 0x1f600, 0x1f64f); +}; + +export const food = () => (n: number = 1): string => { + return randomChar()(n, 0x1f32d, 0x1f37f); +}; + +export const animals = () => (n: number = 1): string => { + return randomChar()(n, 0x1f400, 0x1f4d3); +}; + +export const expressions = () => (n: number = 1): string => { + return randomChar()(n, 0x1f910, 0x1f92f); +}; + +export const gif = (app: any) => (options: any): void => { + const { + url, + posX = 0, + posY = 0, + opacity = 1, + size = "auto", + center = false, + rotation = 0, + filter = 'none', + duration = 10 + } = options; + + let real_duration = duration * app.clock.pulse_duration * app.clock.ppqn; + let fadeOutDuration = real_duration * 0.1; + let visibilityDuration = real_duration - fadeOutDuration; + const gifElement = document.createElement("img"); + gifElement.src = url; + gifElement.style.position = "fixed"; + gifElement.style.left = center ? "50%" : `${posX}px`; + gifElement.style.top = center ? "50%" : `${posY}px`; + gifElement.style.opacity = `${opacity}`; + gifElement.style.zIndex = "1000"; // Ensure it's on top, fixed zIndex + if (size !== "auto") { + gifElement.style.width = size; + gifElement.style.height = size; + } + const transformRules = [`rotate(${rotation}deg)`]; + if (center) { + transformRules.unshift("translate(-50%, -50%)"); + } + gifElement.style.transform = transformRules.join(" "); + gifElement.style.filter = filter; + gifElement.style.transition = `opacity ${fadeOutDuration}s ease`; + document.body.appendChild(gifElement); + + // Start the fade-out at the end of the visibility duration + setTimeout(() => { + gifElement.style.opacity = "0"; + }, visibilityDuration * 1000); + + // Remove the GIF from the DOM after the fade-out duration + setTimeout(() => { + if (document.body.contains(gifElement)) { + document.body.removeChild(gifElement); + } + }, real_duration * 1000); +}; + +export const scope = (app: any) => (config: OscilloscopeConfig): void => { + /** + * Configures the oscilloscope. + * @param config - The configuration object for the oscilloscope. + */ + app.osc = { + ...app.osc, + ...config, + }; +}; \ No newline at end of file diff --git a/src/API/Console.ts b/src/API/Console.ts new file mode 100644 index 0000000..54358dd --- /dev/null +++ b/src/API/Console.ts @@ -0,0 +1,20 @@ +export const log = (app: any) => (message: any) => { + /** + * Logs a message to the console and app-specific logger. + * @param message - The message to log. + */ + console.log(message); + app._logMessage(message, false); +}; + +export const logOnce = (app: any) => (message: any) => { + /** + * Logs a message to the console and app-specific logger, but only once. + * @param message - The message to log. + */ + if (app.onceEvaluator) { + console.log(message); + app._logMessage(message, false); + app.onceEvaluator = false; + } +}; diff --git a/src/API/Counter.ts b/src/API/Counter.ts new file mode 100644 index 0000000..221d8e5 --- /dev/null +++ b/src/API/Counter.ts @@ -0,0 +1,40 @@ +export const once = (app: any) => (): boolean => { + const firstTime = app.api.onceEvaluator; + app.api.onceEvaluator = false; + return firstTime; +}; + +export const counter = (app: any) => (name: string | number, limit?: number, step?: number): number => { + if (!(name in app.counters)) { + app.counters[name] = { + value: 0, + step: step ?? 1, + limit, + }; + } else { + if (app.counters[name].limit !== limit) { + app.counters[name].value = 0; + app.counters[name].limit = limit; + } + + if (app.counters[name].step !== step) { + app.counters[name].step = step ?? app.counters[name].step; + } + + app.counters[name].value += app.counters[name].step; + + if (app.counters[name].limit !== undefined && app.counters[name].value > app.counters[name].limit) { + app.counters[name].value = 0; + } + } + + return app.counters[name].value; +}; + +export const i = (app: any) => (n?: number) => { + if (n !== undefined) { + app.universes[app.selected_universe].global.evaluations = n; + return app.universes[app.selected_universe]; + } + return app.universes[app.selected_universe].global.evaluations as number; +}; \ No newline at end of file diff --git a/src/API/Drunk.ts b/src/API/Drunk.ts new file mode 100644 index 0000000..680b14c --- /dev/null +++ b/src/API/Drunk.ts @@ -0,0 +1,37 @@ +export const drunk = (app: any) => (n?: number): number => { + /** + * This function sets or returns the current drunk mechanism's value. + * @param n - [optional] The value to set the drunk mechanism to + * @returns The current value of the drunk mechanism + */ + if (n !== undefined) { + app._drunk.position = n; + return app._drunk.getPosition(); + } + app._drunk.step(); + return app._drunk.getPosition(); +}; + +export const drunk_max = (app: any) => (max: number): void => { + /** + * Sets the maximum value of the drunk mechanism. + * @param max - The maximum value of the drunk mechanism + */ + app._drunk.max = max; +}; + +export const drunk_min = (app: any) => (min: number): void => { + /** + * Sets the minimum value of the drunk mechanism. + * @param min - The minimum value of the drunk mechanism + */ + app._drunk.min = min; +}; + +export const drunk_wrap = (app: any) => (wrap: boolean): void => { + /** + * Sets whether the drunk mechanism should wrap around + * @param wrap - Whether the drunk mechanism should wrap around + */ + app._drunk.toggleWrap(wrap); +}; \ No newline at end of file diff --git a/src/API/Filters.ts b/src/API/Filters.ts new file mode 100644 index 0000000..68848ac --- /dev/null +++ b/src/API/Filters.ts @@ -0,0 +1,203 @@ +const _euclidean_cycle = ( + pulses: number, + length: number, + rotate: number = 0, + ): boolean[] => { + if (pulses == length) return Array.from({ length }, () => true); + function startsDescent(list: number[], i: number): boolean { + const length = list.length; + const nextIndex = (i + 1) % length; + return list[i] > list[nextIndex] ? true : false; + } + if (pulses >= length) return [true]; + const resList = Array.from( + { length }, + (_, i) => (((pulses * (i - 1)) % length) + length) % length, + ); + let cycle = resList.map((_, i) => startsDescent(resList, i)); + if (rotate != 0) { + cycle = cycle.slice(rotate).concat(cycle.slice(0, rotate)); + } + return cycle; + } + +export const fullseq = () => (sequence: string, duration: number): boolean | Array => { + if (sequence.split("").every((c) => c === "x" || c === "o")) { + return [...sequence].map((c) => c === "x").beat(duration); + } else { + return false; + } +}; + +export const seq = (app: any) => (expr: string, duration: number = 0.5): boolean => { + let len = expr.length * duration; + let output: number[] = []; + + for (let i = 1; i <= len + 1; i += duration) { + output.push(Math.floor(i * 10) / 10); + } + output.pop(); + + output = output.filter((_, idx) => { + const exprIdx = idx % expr.length; + return expr[exprIdx] === "x"; + }); + + return oncount(app)(output, len); +}; + +export const beat = (app: any) => (n: number | number[] = 1, nudge: number = 0): boolean => { + const nArray = Array.isArray(n) ? n : [n]; + const results: boolean[] = nArray.map( + (value) => + (app.clock.pulses_since_origin - Math.floor(nudge * app.ppqn())) % + Math.floor(value * app.ppqn()) === 0, + ); + return results.some((value) => value === true); +}; + +export const bar = (app: any) => (n: number | number[] = 1, nudge: number = 0): boolean => { + const nArray = Array.isArray(n) ? n : [n]; + const barLength = app.clock.time_signature[1] * app.ppqn(); + const nudgeInPulses = Math.floor(nudge * barLength); + const results: boolean[] = nArray.map( + (value) => + (app.clock.pulses_since_origin - nudgeInPulses) % + Math.floor(value * barLength) === 0, + ); + return results.some((value) => value === true); +}; + +export const pulse = (app: any) => (n: number | number[] = 1, nudge: number = 0): boolean => { + const nArray = Array.isArray(n) ? n : [n]; + const results: boolean[] = nArray.map( + (value) => (app.clock.pulses_since_origin - nudge) % value === 0, + ); + return results.some((value) => value === true); +}; + +export const tick = (app: any) => (tick: number | number[], offset: number = 0): boolean => { + const nArray = Array.isArray(tick) ? tick : [tick]; + const results: boolean[] = nArray.map( + (value) => app.clock.time_position.pulse === value + offset, + ); + return results.some((value) => value === true); +}; + +export const dur = (app: any) => (n: number | number[]): boolean => { + let nums: number[] = Array.isArray(n) ? n : [n]; + return beat(app)(nums.dur(...nums)); +}; + + +export const flip = (app: any) => (chunk: number, ratio: number = 50): boolean => { + let realChunk = chunk * 2; + const time_pos = app.clock.pulses_since_origin; + const full_chunk = Math.floor(realChunk * app.ppqn()); + const threshold = Math.floor((ratio / 100) * full_chunk); + const pos_within_chunk = time_pos % full_chunk; + return pos_within_chunk < threshold; +}; + +export const flipbar = (app: any) => (chunk: number = 1): boolean => { + let realFlip = chunk; + const time_pos = app.clock.time_position.bar; + const current_chunk = Math.floor(time_pos / realFlip); + return current_chunk % 2 === 0; +}; + +export const onbar = (app: any) => ( + bars: number[] | number, + n: number = app.clock.time_signature[0], +): boolean => { + let current_bar = (app.clock.time_position.bar % n) + 1; + return typeof bars === "number" + ? bars === current_bar + : bars.some((b) => b === current_bar); +}; + +export const onbeat = (app: any) => (...beat: number[]): boolean => { + let final_pulses: boolean[] = []; + beat.forEach((b) => { + let beatNumber = b % app.nominator() || app.nominator(); + let integral_part = Math.floor(beatNumber); + integral_part = integral_part === 0 ? app.nominator() : integral_part; + let decimal_part = Math.floor((beatNumber - integral_part) * app.ppqn() + 1); + if (decimal_part <= 0) + decimal_part += app.ppqn() * app.nominator(); + final_pulses.push( + integral_part === app.cbeat() && app.cpulse() === decimal_part, + ); + }); + return final_pulses.some((p) => p === true); +}; + +export const oncount = (app: any) => (beats: number[] | number, count: number): boolean => { + if (typeof beats === "number") beats = [beats]; + const origin = app.clock.pulses_since_origin; + let final_pulses: boolean[] = []; + beats.forEach((b) => { + b = b < 1 ? 0 : b - 1; + const beatInTicks = Math.ceil(b * app.ppqn()); + const meterPosition = origin % (app.ppqn() * count); + final_pulses.push(meterPosition === beatInTicks); + }); + return final_pulses.some((p) => p === true); +}; + +export const oneuclid = (app: any) => (pulses: number, length: number, rotate: number = 0): boolean => { + const cycle = app._euclidean_cycle(pulses, length, rotate); + const beats = cycle.reduce((acc: number[], x: boolean, i: number) => { + if (x) acc.push(i + 1); + return acc; + }, []); + return oncount(app)(beats, length); +}; + +export const euclid = (app: any) => (iterator: number, pulses: number, length: number, rotate: number = 0): boolean => { + /** + * Returns a Euclidean cycle of size length, with n pulses, rotated or not. + */ + return app._euclidean_cycle(pulses, length, rotate)[iterator % length]; +}; +export const ec = euclid; + +export const rhythm = (app: any) => (div: number, pulses: number, length: number, rotate: number = 0): boolean => { + /** + * Returns a rhythm based on Euclidean cycle. + */ + return ( + app.beat(div) && app._euclidean_cycle(pulses, length, rotate).beat(div) + ); +}; +export const ry = rhythm; + +export const nrhythm = (app: any) => (div: number, pulses: number, length: number, rotate: number = 0): boolean => { + /** + * Returns a negated rhythm based on Euclidean cycle. + */ + let rhythm = app._euclidean_cycle(pulses, length, rotate).map((n: any) => !n); + return ( + app.beat(div) && rhythm.beat(div) + ); +}; +export const nry = nrhythm; + +export const bin = () => (iterator: number, n: number): boolean => { + /** + * Returns a binary cycle of size n. + */ + let convert: string = n.toString(2); + let tobin: boolean[] = convert.split("").map((x: string) => x === "1"); + return tobin[iterator % tobin.length]; +}; + +export const binrhythm = (app: any) => (div: number, n: number): boolean => { + /** + * Returns a binary rhythm based on division and binary cycle. + */ + let convert: string = n.toString(2); + let tobin: boolean[] = convert.split("").map((x: string) => x === "1"); + return app.beat(div) && tobin.beat(div); +}; +export const bry = binrhythm; \ No newline at end of file diff --git a/src/API/LFO.ts b/src/API/LFO.ts new file mode 100644 index 0000000..d853792 --- /dev/null +++ b/src/API/LFO.ts @@ -0,0 +1,65 @@ +export const line = (app: any) => (start: number, end: number, step: number = 1): number[] => { + const countPlaces = (num: number) => { + var text = num.toString(); + var index = text.indexOf("."); + return index == -1 ? 0 : (text.length - index - 1); + }; + + const result: number[] = []; + + if ((end > start && step > 0) || (end < start && step < 0)) { + for (let value = start; value <= end; value += step) { + result.push(value); + } + } else if ((end > start && step < 0) || (end < start && step > 0)) { + for (let value = start; value >= end; value -= step) { + result.push(parseFloat(value.toFixed(countPlaces(step)))); + } + } else { + console.error("Invalid range or step provided."); + } + + return result; +}; + +export const sine = (app: any) => (freq: number = 1, phase: number = 0): number => { + return Math.sin(2 * Math.PI * freq * (app.clock.ctx.currentTime - phase)); +}; + +export const usine = (app: any) => (freq: number = 1, phase: number = 0): number => { + return ((sine(app)(freq, phase) + 1) / 2); +}; + +export const saw = (app: any) => (freq: number = 1, phase: number = 0): number => { + return (((app.clock.ctx.currentTime * freq + phase) % 1) * 2 - 1); +}; + +export const usaw = (app: any) => (freq: number = 1, phase: number = 0): number => { + return ((saw(app)(freq, phase) + 1) / 2); +}; + +export const triangle = (app: any) => (freq: number = 1, phase: number = 0): number => { + return (Math.abs(saw(app)(freq, phase)) * 2 - 1); +}; + +export const utriangle = (app: any) => (freq: number = 1, phase: number = 0): number => { + return ((triangle(app)(freq, phase) + 1) / 2); +}; + +export const square = (app: any) => (freq: number = 1, duty: number = 0.5): number => { + const period = 1 / freq; + const t = (Date.now() / 1000) % period; + return (t / period < duty ? 1 : -1); +}; + +export const usquare = (app: any) => (freq: number = 1, duty: number = 0.5): number => { + return ((square(app)(freq, duty) + 1) / 2); +}; + +export const noise = (app: any) => (): number => { + return (app.randomGen() * 2 - 1); // Assuming randomGen() is defined in the app context +}; + +export const unoise = (app: any) => (): number => { + return ((noise(app)() + 1) / 2); +}; diff --git a/src/API/MIDI.ts b/src/API/MIDI.ts new file mode 100644 index 0000000..6a69a3e --- /dev/null +++ b/src/API/MIDI.ts @@ -0,0 +1,199 @@ +import { getAllScaleNotes } from 'zifferjs'; +import { + MidiCCEvent, + MidiNoteEvent, +} from "../IO/MidiConnection"; +import { MidiEvent, MidiParams } from "../Classes/MidiEvent"; + +interface ControlChange { + channel: number; + control: number; + value: number; +} + + +export const midi_outputs = (app: any) => (): void => { + app._logMessage(app.MidiConnection.listMidiOutputs(), false); +}; + +export const midi_output = (app: any) => (outputName: string): void => { + if (!outputName) { + console.log(app.MidiConnection.getCurrentMidiPort()); + } else { + app.MidiConnection.switchMidiOutput(outputName); + } +}; + +export const midi = (app: any) => ( + value: number | number[] = 60, + velocity?: number | number[], + channel?: number | number[], + port?: number | string | number[] | string[], +): MidiEvent => { + const event = { note: value, velocity, channel, port } as MidiParams; + return new MidiEvent(event, app); +}; + +export const sysex = (app: any) => (data: Array): void => { + app.MidiConnection.sendSysExMessage(data); +}; + +export const pitch_bend = (app: any) => (value: number, channel: number): void => { + app.MidiConnection.sendPitchBend(value, channel); +}; + +export const program_change = (app: any) => (program: number, channel: number): void => { + app.MidiConnection.sendProgramChange(program, channel); +}; + +export const midi_clock = (app: any) => (): void => { + app.MidiConnection.sendMidiClock(); +}; + +export const control_change = (app: any) => ({ + control = 20, + value = 0, + channel = 0, +}: ControlChange): void => { + app.MidiConnection.sendMidiControlChange(control, value, channel); +}; + +export const cc = control_change; + +export const midi_panic = (app: any) => (): void => { + app.MidiConnection.panic(); +}; + +export const active_note_events = (app: any) => ( + channel?: number, +): MidiNoteEvent[] | undefined => { + let events; + if (channel) { + events = app.MidiConnection.activeNotesFromChannel(channel); + } else { + events = app.MidiConnection.activeNotes; + } + if (events.length > 0) return events; + else return undefined; +}; + +export const transmission = (app: any) => (): boolean => { + return app.MidiConnection.activeNotes.length > 0; +}; + +export const active_notes = (app: any) => (channel?: number): number[] | undefined => { + const events = active_note_events(app)(channel); + if (events && events.length > 0) return events.map((e) => e.note); + else return undefined; +}; + +export const kill_active_notes = (app: any) => (): void => { + app.MidiConnection.activeNotes = []; +}; + +export const sticky_notes = (app: any) => (channel?: number): number[] | undefined => { + let notes; + if (channel) notes = app.MidiConnection.stickyNotesFromChannel(channel); + else notes = app.MidiConnection.stickyNotes; + if (notes.length > 0) return notes.map((e: any) => e.note); + else return undefined; +}; + +export const kill_sticky_notes = (app: any) => (): void => { + app.MidiConnection.stickyNotes = []; +}; + +export const buffer = (app: any) => (channel?: number): boolean => { + if (channel) + return ( + app.MidiConnection.findNoteFromBufferInChannel(channel) !== undefined + ); + else return app.MidiConnection.noteInputBuffer.length > 0; +}; + +export const buffer_event = (app: any) => (channel?: number): MidiNoteEvent | undefined => { + if (channel) + return app.MidiConnection.findNoteFromBufferInChannel(channel); + else return app.MidiConnection.noteInputBuffer.shift(); +}; + +export const buffer_note = (app: any) => (channel?: number): number | undefined => { + const note = buffer_event(app)(channel); + return note ? note.note : undefined; +}; + +export const last_note_event = (app: any) => (channel?: number): MidiNoteEvent | undefined => { + if (channel) return app.MidiConnection.lastNoteInChannel[channel]; + else return app.MidiConnection.lastNote; +}; + +export const last_note = (app: any) => (channel?: number): number => { + const note = last_note_event(app)(channel); + return note ? note.note : 60; +}; + +export const ccIn = (app: any) => (control: number, channel?: number): number => { + if (channel) { + if (app.MidiConnection.lastCCInChannel[channel]) { + return app.MidiConnection.lastCCInChannel[channel][control]; + } else return 0; + } else return app.MidiConnection.lastCC[control] || 0; +}; + +export const has_cc = (app: any) => (channel?: number): boolean => { + if (channel) + return ( + app.MidiConnection.findCCFromBufferInChannel(channel) !== undefined + ); + else return app.MidiConnection.ccInputBuffer.length > 0; +}; + +export const buffer_cc = (app: any) => (channel?: number): MidiCCEvent | undefined => { + if (channel) return app.MidiConnection.findCCFromBufferInChannel(channel); + else return app.MidiConnection.ccInputBuffer.shift(); +}; + +export const show_scale = (app: any) => ( + root: number | string, + scale: number | string, + channel: number = 0, + port: number | string = app.MidiConnection.currentOutputIndex || 0, + soundOff: boolean = false, +): void => { + if (!app.scale_aid || scale !== app.scale_aid) { + hide_scale(app)(root, scale, channel, port); + const scaleNotes = getAllScaleNotes(scale, root); + scaleNotes.forEach((note) => { + app.MidiConnection.sendMidiOn(note, channel, 1, port); + if (soundOff) app.MidiConnection.sendAllSoundOff(channel, port); + }); + app.scale_aid = scale; + } +}; + +export const hide_scale = (app: any) => ( + root: number | string = 0, + scale: number | string = 0, + channel: number = 0, + port: number | string = app.MidiConnection.currentOutputIndex || 0, +): void => { + const allNotes = Array.from(Array(128).keys()); + allNotes.forEach((note) => { + app.MidiConnection.sendMidiOff(note, channel, port); + }); + app.scale_aid = undefined; +}; + +export const midi_notes_off = (app: any) => ( + channel: number = 0, + port: number | string = app.MidiConnection.currentOutputIndex || 0, +): void => { + app.MidiConnection.sendAllNotesOff(channel, port); +}; + +export const midi_sound_off = (app: any) => ( + channel: number = 0, + port: number | string = app.MidiConnection.currentOutputIndex || 0, +): void => { + app.MidiConnection.sendAllSoundOff(channel, port); +}; diff --git a/src/API/Math.ts b/src/API/Math.ts new file mode 100644 index 0000000..e38eaf8 --- /dev/null +++ b/src/API/Math.ts @@ -0,0 +1,36 @@ +// mathFunctions.ts +export const min = (app: any) => (...values: number[]): number => { + /** + * Returns the minimum value of a list of numbers. + */ + return Math.min(...values); +}; + +export const max = (app: any) => (...values: number[]): number => { + /** + * Returns the maximum value of a list of numbers. + */ + return Math.max(...values); +}; + +export const mean = (app: any) => (...values: number[]): number => { + /** + * Returns the mean of a list of numbers. + */ + const sum = values.reduce((accumulator, currentValue) => accumulator + currentValue, 0); + return values.length > 0 ? sum / values.length : 0; +}; + +export const limit = (app: any) => (value: number, min: number, max: number): number => { + /** + * Limits a value between a minimum and a maximum. + */ + return Math.min(Math.max(value, min), max); +}; + +export const abs = (app: any) => (value: number): number => { + /** + * Returns the absolute value of a number. + */ + return Math.abs(value); +}; \ No newline at end of file diff --git a/src/API/Mouse.ts b/src/API/Mouse.ts new file mode 100644 index 0000000..c7506fd --- /dev/null +++ b/src/API/Mouse.ts @@ -0,0 +1,33 @@ +// mouse.ts +export const onmousemove = (app: any) => (e: MouseEvent): void => { + app._mouseX = e.pageX; + app._mouseY = e.pageY; +}; + +export const mouseX = (app: any) => (): number => { + /** + * @returns The current x position of the mouse + */ + return app._mouseX; +}; + +export const mouseY = (app: any) => (): number => { + /** + * @returns The current y position of the mouse + */ + return app._mouseY; +}; + +export const noteX = (app: any) => (): number => { + /** + * @returns The current x position scaled to 0-127 using screen width + */ + return Math.floor((app._mouseX / document.body.clientWidth) * 127); +}; + +export const noteY = (app: any) => (): number => { + /** + * @returns The current y position scaled to 0-127 using screen height + */ + return Math.floor((app._mouseY / document.body.clientHeight) * 127); +}; \ No newline at end of file diff --git a/src/API/OSC.ts b/src/API/OSC.ts new file mode 100644 index 0000000..7a66b05 --- /dev/null +++ b/src/API/OSC.ts @@ -0,0 +1,27 @@ +import { sendToServer, type OSCMessage } from "../IO/OSC"; + +export const osc = (app: any) => (address: string, port: number, ...args: any[]): void => { + /** + * Sends an OSC message to the server. + */ + sendToServer({ + address: address, + port: port, + args: args, + timetag: Math.round(Date.now() + (app.clock.nudge - app.clock.deviation)), + } as OSCMessage); +}; + +export const getOSC = (app: any) => (address?: string): any[] => { + /** + * Retrieves incoming OSC messages. Filters by address if provided. + */ + let oscMessages = app.oscMessages; // Assuming `oscMessages` is stored in `app` + if (address) { + let messages = oscMessages.filter((msg: { address: string; }) => msg.address === address); + messages = messages.map((msg: { data: any; }) => msg.data); + return messages; + } else { + return oscMessages; + } +}; \ No newline at end of file diff --git a/src/API/Probabilities.ts b/src/API/Probabilities.ts new file mode 100644 index 0000000..76d6142 --- /dev/null +++ b/src/API/Probabilities.ts @@ -0,0 +1,53 @@ +// Probability.ts + +export const prob = (app: any) => (p: number): boolean => { + return app.randomGen() * 100 < p; +}; + +export const toss = (app: any) => (): boolean => { + return app.randomGen() > 0.5; +}; + +export const odds = (app: any) => (n: number, beats: number = 1): boolean => { + return app.randomGen() < (n * app.ppqn()) / (app.ppqn() * beats); +}; + +export const never = (app: any) => (beats: number = 1): boolean => { + return false; +}; + +export const almostNever = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.025 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const rarely = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.1 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const scarcely = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.25 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const sometimes = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.5 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const often = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.75 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const frequently = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.9 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const almostAlways = (app: any) => (beats: number = 1): boolean => { + return app.randomGen() < (0.985 * app.ppqn()) / (app.ppqn() * beats); +}; + +export const always = (app: any) => (beats: number = 1): boolean => { + return true; +}; + +export const dice = (app: any) => (sides: number): number => { + return Math.floor(app.randomGen() * sides) + 1; +}; \ No newline at end of file diff --git a/src/API/Randomness.ts b/src/API/Randomness.ts new file mode 100644 index 0000000..d8fb82c --- /dev/null +++ b/src/API/Randomness.ts @@ -0,0 +1,33 @@ +import { seededRandom } from "zifferjs"; + +export const randI = (app: any) => (min: number, max: number): number => { + return Math.floor(app.randomGen() * (max - min + 1)) + min; +}; + +export const rand = (app: any) => (min: number, max: number): number => { + return app.randomGen() * (max - min) + min; +}; + +export const seed = (app: any) => (seed: string | number): void => { + if (typeof seed === "number") seed = seed.toString(); + if (app.currentSeed !== seed) { + app.currentSeed = seed; + app.randomGen = seededRandom(seed); + } +}; + +export const localSeededRandom = (app: any) => (seed: string | number): Function => { + if (typeof seed === "number") seed = seed.toString(); + if (app.localSeeds.has(seed)) return app.localSeeds.get(seed) as Function; + const newSeededRandom = seededRandom(seed); + app.localSeeds.set(seed, newSeededRandom); + return newSeededRandom; +}; + +export const clearLocalSeed = (app: any) => (seed: string | number | undefined = undefined): void => { + if (seed) { + app.localSeeds.delete(seed.toString()); + } else { + app.localSeeds.clear(); + } +}; \ No newline at end of file diff --git a/src/API/Script.ts b/src/API/Script.ts new file mode 100644 index 0000000..a25f19d --- /dev/null +++ b/src/API/Script.ts @@ -0,0 +1,63 @@ +import { tryEvaluate } from "../Evaluator"; +import { blinkScript } from "../DOM/Visuals/Blinkers"; +import { template_universes } from "../Editor/FileManagement"; + +export const script = (app: any) => (...args: number[]): void => { + args.forEach((arg) => { + if (arg >= 1 && arg <= 9) { + blinkScript(app, "local", arg); + tryEvaluate( + app, + app.universes[app.selected_universe].locals[arg], + ); + } + }); +}; + +export const s = script; + +export const delete_script = (app: any) => (script: number): void => { + app.universes[app.selected_universe].locals[script] = { + candidate: "", + committed: "", + evaluations: 0, + }; +}; + +export const copy_script = (app: any) => (from: number, to: number): void => { + app.universes[app.selected_universe].locals[to] = { + ...app.universes[app.selected_universe].locals[from], + }; +}; + +export const copy_universe = (app: any) => (from: string, to: string): void => { + app.universes[to] = { + ...app.universes[from], + }; +}; + +export const delete_universe = (app: any) => (universe: string): void => { + if (app.selected_universe === universe) { + app.selected_universe = "Default"; + } + delete app.universes[universe]; + app.settings.saveApplicationToLocalStorage( + app.universes, + app.settings, + ); + app.updateKnownUniversesView(); +}; + +export const big_bang = (app: any) => (): void => { + if (confirm("Are you sure you want to delete all universes?")) { + app.universes = { + ...template_universes, // Assuming template_universes is defined elsewhere + }; + app.settings.saveApplicationToLocalStorage( + app.universes, + app.settings, + ); + } + app.selected_universe = "Default"; + app.updateKnownUniversesView(); +}; \ No newline at end of file diff --git a/src/API/Sound.ts b/src/API/Sound.ts new file mode 100644 index 0000000..b547a9f --- /dev/null +++ b/src/API/Sound.ts @@ -0,0 +1,43 @@ +import { SoundEvent } from "../Classes/SoundEvent"; +import { SkipEvent } from "../Classes/SkipEvent"; + +export const sound = (app: any) => (sound: string | string[] | null | undefined) => { + /** + * Creates a sound event if a sound is specified, otherwise returns a skip event. + * @param sound - The sound identifier or array of identifiers to play. + * @returns SoundEvent if sound is defined, otherwise SkipEvent. + */ + if (sound) return new SoundEvent(sound, app); + else return new SkipEvent(); +}; + +export const snd = sound; + +export const speak = (app: any) => (text: string, lang: string = "en-US", voiceIndex: number = 0, rate: number = 1, pitch: number = 1): void => { + /** + * Speaks the given text using the browser's speech synthesis API. + * @param text - The text to speak. + * @param lang - The language code (e.g., "en-US"). + * @param voiceIndex - The index of the voice to use from the speechSynthesis voice list. + * @param rate - The rate at which to speak the text. + * @param pitch - The pitch at which to speak the text. + */ + const msg = new SpeechSynthesisUtterance(text); + msg.lang = lang; + msg.rate = rate; + msg.pitch = pitch; + + // Set the voice using a provided index + const voices = window.speechSynthesis.getVoices(); + msg.voice = voices[voiceIndex] || null; + + window.speechSynthesis.speak(msg); + + msg.onend = () => { + console.log("Finished speaking:", text); + }; + + msg.onerror = (event) => { + console.error("Speech synthesis error:", event); + }; +}; \ No newline at end of file diff --git a/src/API/Theme.ts b/src/API/Theme.ts new file mode 100644 index 0000000..3a54c56 --- /dev/null +++ b/src/API/Theme.ts @@ -0,0 +1,30 @@ +import { type Editor } from '../main'; +import colorschemes from "../Editor/colors.json"; + +export const theme = (app: Editor) => (color_scheme: string): void => { + app.readTheme(color_scheme); +}; + +export const themeName = (app: Editor) => (): string => { + return app.currentThemeName; +}; + +export const randomTheme = (app: Editor) => (): void => { + let theme_names = getThemes()(); + let selected_theme = theme_names[Math.floor(Math.random() * theme_names.length)]; + app.readTheme(selected_theme); +}; + +export const nextTheme = (app: Editor) => (): void => { + let theme_names = getThemes()(); + let current_theme = themeName(app)(); + let current_theme_idx = theme_names.indexOf(current_theme); + let next_theme_idx = (current_theme_idx + 1) % theme_names.length; + let next_theme = theme_names[next_theme_idx]; + app.readTheme(next_theme); + app.api.log(next_theme); +}; + +export const getThemes = () => (): string[] => { + return Object.keys(colorschemes); +}; \ No newline at end of file diff --git a/src/API/Transport.ts b/src/API/Transport.ts new file mode 100644 index 0000000..3e6f1d7 --- /dev/null +++ b/src/API/Transport.ts @@ -0,0 +1,76 @@ +export const time = (app: any) => (): number => { + return app.audioContext.currentTime; +}; + +export const play = (app: any) => (): void => { + app.setButtonHighlighting("play", true); + app.MidiConnection.sendStartMessage(); + app.clock.start(); +}; + +export const pause = (app: any) => (): void => { + app.setButtonHighlighting("pause", true); + app.clock.pause(); +}; + +export const stop = (app: any) => (): void => { + app.setButtonHighlighting("stop", true); + app.clock.stop(); +}; + +export const silence = (app: any) => (): void => { + return stop(app)(); +}; + +export const tempo = (app: any) => (n?: number): number => { + /** + * Sets or returns the current bpm. + */ + if (n === undefined) return app.clock.bpm; + + if (n >= 1 && n <= 500) { + app.clock.bpm = n; + } else { + console.error("BPM out of acceptable range (1-500)."); + } + return n; +}; + +export const bpb = (app: any) => (n?: number): number => { + /** + * Sets or returns the number of beats per bar. + */ + if (n === undefined) return app.clock.time_signature[0]; + + if (n >= 1) { + app.clock.time_signature[0] = n; + } else { + console.error("Beats per bar must be at least 1."); + } + return n; +}; + +export const ppqn = (app: any) => (n?: number): number => { + /** + * Sets or returns the number of pulses per quarter note. + */ + if (n === undefined) return app.clock.ppqn; + + if (n >= 1) { + app.clock.ppqn = n; + } else { + console.error("Pulses per quarter note must be at least 1."); + } + return n; +}; + +export const time_signature = (app: any) => (numerator: number, denominator: number): void => { + /** + * Sets the time signature. + */ + if (numerator < 1 || denominator < 1) { + console.error("Time signature values must be at least 1."); + } else { + app.clock.time_signature = [numerator, denominator]; + } +}; diff --git a/src/API/Warp.ts b/src/API/Warp.ts new file mode 100644 index 0000000..7de5e8d --- /dev/null +++ b/src/API/Warp.ts @@ -0,0 +1,16 @@ +export const warp = (app: any) => (n: number): void => { + /** + * Time-warp the clock by using the tick you wish to jump to. + */ + app.clock.tick = n; + app.clock.time_position = app.clock.convertTicksToTimeposition(n); +}; + +export const beat_warp = (app: any) => (beat: number): void => { + /** + * Time-warp the clock by using the tick you wish to jump to. + */ + const ticks = beat * app.clock.ppqn; + app.clock.tick = ticks; + app.clock.time_position = app.clock.convertTicksToTimeposition(ticks); +}; \ No newline at end of file diff --git a/src/API/Ziffers.ts b/src/API/Ziffers.ts new file mode 100644 index 0000000..508f8d4 --- /dev/null +++ b/src/API/Ziffers.ts @@ -0,0 +1,73 @@ +import { InputOptions, Player } from "../Classes/ZPlayer"; +import { generateCacheKey, removePatternFromCache } from "./Cache" + + +// ziffersFunctions.ts +export const z = (app: any) => (input: string | Generator, options: InputOptions = {}, id: number | string = ""): Player => { + const zid = "z" + id.toString(); + const key = id === "" ? generateCacheKey(app)(input, options) : zid; + + const validSyntax = typeof input === "string" && !app.invalidPatterns[input] + + let player; + let replace = false; + + if (app.patternCache.has(key)) { + player = app.patternCache.get(key) as Player; + + if (typeof input === "string" && + player.input !== input && + (player.atTheBeginning() || app.forceEvaluator)) { + replace = true; + } + } + + if ((typeof input !== "string" || validSyntax) && (!player || replace)) { + if (typeof input === "string" && player && app.forceEvaluator) { + if (!player.updatePattern(input, options)) { + app.logOnce(`Invalid syntax: ${input}`); + }; + app.forceEvaluator = false; + } else { + const newPlayer = player ? new Player(input, options, app, zid, player.nextEndTime()) : new Player(input, options, app, zid); + if (newPlayer.isValid()) { + player = newPlayer; + app.patternCache.set(key, player); + } else if (typeof input === "string") { + app.invalidPatterns[input] = true; + } + } + } + + if (player) { + if (player.atTheBeginning()) { + if (typeof input === "string" && !validSyntax) app.log(`Invalid syntax: ${input}`); + } + + if (player.ziffers.generator && player.ziffers.generatorDone) { + removePatternFromCache(app)(key); + } + + if (typeof id === "number") player.zid = zid; + + player.updateLastCallTime(); + + if (id !== "" && zid !== "z0") { + // Sync named patterns to z0 by default + player.sync("z0", false); + } + + return player; + } else { + throw new Error(`Invalid syntax: ${input}`); + } +}; + +// Generating numbered functions dynamically +export const generateZFunctions = (app: any) => { + const zFunctions: { [key: string]: (input: string, opts: InputOptions) => Player } = {}; + for (let i = 0; i <= 16; i++) { + zFunctions[`z${i}`] = (input: string, opts: InputOptions = {}) => z(app)(input, opts, i); + } + return zFunctions; +}; diff --git a/src/DOM/UILogic.ts b/src/DOM/UILogic.ts index a3f86fd..3acdd28 100644 --- a/src/DOM/UILogic.ts +++ b/src/DOM/UILogic.ts @@ -19,7 +19,7 @@ import { closeUniverseModal, openUniverseModal, } from "../Editor/FileManagement"; -import { loadSamples } from "../API"; +import { loadSamples } from "../API/API"; import { tryEvaluate } from "../Evaluator"; import { inlineHoveringTips } from "../Docs/inlineHelp"; import { lineNumbers } from "@codemirror/view"; diff --git a/src/Docs/patterns/functions.ts b/src/Docs/patterns/functions.ts index 971fd90..6bc515a 100644 --- a/src/Docs/patterns/functions.ts +++ b/src/Docs/patterns/functions.ts @@ -53,32 +53,5 @@ ${makeExample( `usine(1/2).linlin(0, 1, 0, 100)`, true, )} - - - -## Delay functions - -- delay(ms: number, func: Function): void: Delays the execution of a function by a given number of milliseconds. - -${makeExample( - "Phased woodblocks", - ` -// Some very low-budget version of phase music -beat(.5) :: delay(usine(.125) * 80, () => sound('east').out()) -beat(.5) :: delay(50, () => sound('east').out()) -`, - true, - )} - -- delayr(ms: number, nb: number, func: Function): void: Delays the execution of a function by a given number of milliseconds, repeated a given number of times. - -${makeExample( - "Another woodblock texture", - ` -beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out()) -flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out()) -`, - true, - )}; `; }; diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 8fbae25..0d29ff8 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,4 +1,4 @@ -import { UserAPI } from "../API"; +import { UserAPI } from "../API/API"; import { AppSettings } from "../Editor/FileManagement"; export type MidiNoteEvent = { @@ -64,7 +64,7 @@ export class MidiConnection { constructor(api: UserAPI, settings: AppSettings) { this.api = api; this.settings = settings; - this.lastBPM = api.tempo(); + this.lastBPM = api.app.clock.bpm; this.roundedBPM = this.lastBPM; this.initializeMidiAccess(); } @@ -294,33 +294,33 @@ export class MidiConnection { const message = event as MIDIMessageEvent; /* MIDI CLOCK */ if (input.name === this.settings.midi_clock_input) { - if (message.data[0] === 0xf8) { + if (message.data![0] === 0xf8) { if (this.skipOnError > 0) { this.skipOnError -= 1; } else { this.onMidiClock(event.timeStamp); } - } else if (message.data[0] === 0xfa) { + } else if (message.data![0] === 0xfa) { console.log("MIDI start received"); this.api.stop(); this.api.play(); - } else if (message.data[0] === 0xfc) { + } else if (message.data![0] === 0xfc) { console.log("MIDI stop received"); this.api.pause(); - } else if (message.data[0] === 0xfb) { + } else if (message.data![0] === 0xfb) { console.log("MIDI continue received"); this.api.play(); - } else if (message.data[0] === 0xfe) { + } else if (message.data![0] === 0xfe) { console.log("MIDI active sensing received"); } } /* DEFAULT MIDI INPUT */ if (input.name === this.settings.default_midi_input) { // If message is one of note ons - if (message.data[0] >= 0x90 && message.data[0] <= 0x9f) { - const channel = message.data[0] - 0x90 + 1; - const note = message.data[1]; - const velocity = message.data[2]; + if (message.data![0] >= 0x90 && message.data![0] <= 0x9f) { + const channel = message.data![0] - 0x90 + 1; + const note = message.data![1]; + const velocity = message.data![2]; this.lastNote = { note, @@ -361,17 +361,17 @@ export class MidiConnection { } // If note off - if (message.data[0] >= 0x80 && message.data[0] <= 0x8f) { - const channel = message.data[0] - 0x80 + 1; - const note = message.data[1]; + if (message.data![0] >= 0x80 && message.data![0] <= 0x8f) { + const channel = message.data![0] - 0x80 + 1; + const note = message.data![1]; this.removeFromActiveNotes(note, channel); } // If message is one of CCs - if (message.data[0] >= 0xb0 && message.data[0] <= 0xbf) { - const channel = message.data[0] - 0xb0 + 1; - const control = message.data[1]; - const value = message.data[2]; + if (message.data![0] >= 0xb0 && message.data![0] <= 0xbf) { + const channel = message.data![0] - 0xb0 + 1; + const control = message.data![1]; + const value = message.data![2]; this.lastCC[control] = value; if (this.lastCCInChannel[channel]) { diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 0b995c5..da482b0 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -1,5 +1,5 @@ import { type Editor } from "../main"; -import { AudibleEvent } from "./AbstractEvents"; +import { AudibleEvent } from "../Classes/AbstractEvents"; import { sendToServer, type OSCMessage } from "../IO/OSC"; import { filterObject, diff --git a/src/extensions/ArrayExtensions.ts b/src/extensions/ArrayExtensions.ts index 39886ce..352fdf9 100644 --- a/src/extensions/ArrayExtensions.ts +++ b/src/extensions/ArrayExtensions.ts @@ -1,4 +1,4 @@ -import { type UserAPI } from "../API"; +import { type UserAPI } from "../API/API"; import { safeScale, stepsToScale } from "zifferjs"; export { }; diff --git a/src/extensions/NumberExtensions.ts b/src/extensions/NumberExtensions.ts index 94167df..1f1d1d7 100644 --- a/src/extensions/NumberExtensions.ts +++ b/src/extensions/NumberExtensions.ts @@ -1,4 +1,4 @@ -import { type UserAPI } from "../API"; +import { type UserAPI } from "../API/API"; import { MidiEvent } from "../Classes/MidiEvent"; import { Player } from "../Classes/ZPlayer"; import { SoundEvent } from "../Classes/SoundEvent"; diff --git a/src/extensions/StringExtensions.ts b/src/extensions/StringExtensions.ts index 6a6f10d..cdd44af 100644 --- a/src/extensions/StringExtensions.ts +++ b/src/extensions/StringExtensions.ts @@ -1,5 +1,5 @@ import { noteNameToMidi } from "zifferjs"; -import { type UserAPI } from "../API"; +import { type UserAPI } from "../API/API"; import { Player } from "../Classes/ZPlayer"; export {}; diff --git a/src/main.ts b/src/main.ts index 07791a3..bec869d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,7 @@ import { installEditor } from "./Editor/EditorSetup"; import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Docs/Documentation"; import { EditorView } from "codemirror"; import { Clock } from "./Clock/Clock"; -import { loadSamples, UserAPI } from "./API"; +import { loadSamples, UserAPI } from "./API/API"; import * as oeis from "jisg"; import * as zpatterns from "zifferjs/src/patterns.ts"; import { makeArrayExtensions } from "./Extensions/ArrayExtensions";