diff --git a/README.md b/README.md index 04805d0..acdce1e 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@

-Topos is a web-based application that lives [here](https://topos.raphaelforment.fr). Documentation and description is directly included in the application itself. +Topos is a web-based live coding environment. It lives [here](https://topos.raphaelforment.fr). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal. -![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/screnshot.png) +![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) ## Disclaimer diff --git a/img/topos_gif.gif b/img/topos_gif.gif new file mode 100644 index 0000000..ad26885 Binary files /dev/null and b/img/topos_gif.gif differ diff --git a/index.html b/index.html index 16fc35f..8ecd55f 100644 --- a/index.html +++ b/index.html @@ -261,6 +261,10 @@ +
+ + +
diff --git a/src/API.ts b/src/API.ts index b62388a..5da3daa 100644 --- a/src/API.ts +++ b/src/API.ts @@ -9,7 +9,7 @@ import { tryEvaluate, evaluateOnce } from "./Evaluator"; import { DrunkWalk } from "./Utils/Drunk"; import { Editor } from "./main"; import { SoundEvent } from "./classes/SoundEvent"; -import { MidiEvent } from "./classes/MidiEvent"; +import { MidiEvent, MidiParams } from "./classes/MidiEvent"; import { LRUCache } from "lru-cache"; import { InputOptions, Player } from "./classes/ZPlayer"; import { @@ -28,6 +28,7 @@ import { import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; +import { SkipEvent } from './classes/SkipEvent'; interface ControlChange { channel: number; @@ -390,9 +391,10 @@ export class UserAPI { }; public midi = ( - value: number | object = 60, - velocity?: number, - channel?: number + value: number | number[] = 60, + velocity?: number | number[], + channel?: number | number[], + port?: number | string | number[] | string[] ): MidiEvent => { /** * Sends a MIDI note to the current MIDI output. @@ -402,24 +404,9 @@ export class UserAPI { * { channel: 0, velocity: 100, duration: 0.5 } */ - if (velocity !== undefined) { - // Check if value is of type number - if (typeof value === "number") { - value = { note: value }; - } - // @ts-ignore - value["velocity"] = velocity; - } + const event = {note: value, velocity, channel, port} as MidiParams - if (channel !== undefined) { - if (typeof value === "number") { - value = { note: value }; - } - // @ts-ignore - value["channel"] = channel; - } - - return new MidiEvent(value, this.app); + return new MidiEvent(event, this.app); }; public sysex = (data: Array): void => { @@ -1893,8 +1880,9 @@ export class UserAPI { // Trivial functions // ============================================================= - sound = (sound: string | object) => { - return new SoundEvent(sound, this.app); + sound = (sound: string | string[] | null | undefined) => { + if(sound) return new SoundEvent(sound, this.app); + else return new SkipEvent(); }; snd = this.sound; diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index c5dddc0..dc0b06b 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -125,6 +125,8 @@ export interface OscilloscopeConfig { size: number; } +let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg' + /** * Initializes and runs an oscilloscope using an AnalyzerNode. * @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope. @@ -163,6 +165,8 @@ export const runOscilloscope = ( } analyzer.getFloatTimeDomainData(dataArray); + canvasCtx.globalCompositeOperation = 'source-over'; + canvasCtx.fillStyle = "rgba(0, 0, 0, 0)"; canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); @@ -179,40 +183,65 @@ export const runOscilloscope = ( } else { canvasCtx.strokeStyle = app.osc.color; } + const remainingRefreshTime = app.clock.time_position.pulse % app.osc.refresh; + const opacityRatio = 1 - (remainingRefreshTime / app.osc.refresh); + canvasCtx.globalAlpha = opacityRatio; canvasCtx.beginPath(); - // Drawing logic varies based on orientation and 3D setting + + let startIndex = 0; + for (let i = 1; i < dataArray.length; ++i) { + let currentType = null; + if (dataArray[i] >= 0 && dataArray[i - 1] < 0) { + currentType = 'negToPos'; + } else if (dataArray[i] < 0 && dataArray[i - 1] >= 0) { + currentType = 'posToNeg'; + } + + if (currentType) { + if (lastZeroCrossingType === null || currentType === lastZeroCrossingType) { + startIndex = i; + lastZeroCrossingType = currentType; + break; + } + } + } + + if (app.osc.is3D) { - for (let i = 0; i < dataArray.length; i += 2) { + for (let i = startIndex; i < dataArray.length; i += 2) { const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4; const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4; - i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); + i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); } } else if (app.osc.orientation === "horizontal") { - let x = 0; const sliceWidth = (WIDTH * 1.0) / dataArray.length; const yOffset = HEIGHT / 4; - for (let i = 0; i < dataArray.length; i++) { + let x = 0; + for (let i = startIndex; i < dataArray.length; i++) { const v = dataArray[i] * 0.5 * HEIGHT * app.osc.size; const y = v + yOffset; - i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); + i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); x += sliceWidth; } canvasCtx.lineTo(WIDTH, yOffset); } else { - let y = 0; const sliceHeight = (HEIGHT * 1.0) / dataArray.length; const xOffset = WIDTH / 4; - for (let i = 0; i < dataArray.length; i++) { + let y = 0; + for (let i = startIndex; i < dataArray.length; i++) { const v = dataArray[i] * 0.5 * WIDTH * app.osc.size; const x = v + xOffset; - i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); + i === startIndex ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); y += sliceHeight; } canvasCtx.lineTo(xOffset, HEIGHT); } canvasCtx.stroke(); + canvasCtx.globalAlpha = 1.0; } + + draw(); }; diff --git a/src/DomElements.ts b/src/DomElements.ts index de5b0ce..964711c 100644 --- a/src/DomElements.ts +++ b/src/DomElements.ts @@ -1,12 +1,12 @@ export type ElementMap = { [key: string]: - | HTMLElement - | HTMLButtonElement - | HTMLDivElement - | HTMLInputElement - | HTMLSelectElement - | HTMLCanvasElement - | HTMLFormElement; + | HTMLElement + | HTMLButtonElement + | HTMLDivElement + | HTMLInputElement + | HTMLSelectElement + | HTMLCanvasElement + | HTMLFormElement; }; export const singleElements = { @@ -36,6 +36,7 @@ export const singleElements = { line_numbers_checkbox: "show-line-numbers", time_position_checkbox: "show-time-position", tips_checkbox: "show-tips", + completion_checkbox: "show-completions", midi_clock_checkbox: "send-midi-clock", midi_channels_scripts: "midi-channels-scripts", midi_clock_ppqn: "midi-clock-ppqn-input", diff --git a/src/EditorSetup.ts b/src/EditorSetup.ts index 944454d..5e3be76 100644 --- a/src/EditorSetup.ts +++ b/src/EditorSetup.ts @@ -34,6 +34,12 @@ import { EditorView } from "codemirror"; import { toposTheme } from "./themes/toposTheme"; import { javascript } from "@codemirror/lang-javascript"; import { inlineHoveringTips } from "./documentation/inlineHelp"; +import { toposCompletions } from "./documentation/inlineHelp"; +import { javascriptLanguage } from "@codemirror/lang-javascript" + +export const jsCompletions = javascriptLanguage.data.of({ + autocomplete: toposCompletions +}) export const editorSetup: Extension = (() => [ highlightActiveLineGutter(), @@ -47,8 +53,6 @@ export const editorSetup: Extension = (() => [ bracketMatching(), closeBrackets(), autocompletion(), - // rectangularSelection(), - // crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), keymap.of([ @@ -62,6 +66,7 @@ export const editorSetup: Extension = (() => [ export const installEditor = (app: Editor) => { app.vimModeCompartment = new Compartment(); app.hoveringCompartment = new Compartment(); + app.completionsCompartment = new Compartment(); app.withLineNumbers = new Compartment(); app.chosenLanguage = new Compartment(); app.fontSize = new Compartment(); @@ -86,6 +91,7 @@ export const installEditor = (app: Editor) => { app.withLineNumbers.of(lines), app.fontSize.of(fontModif), app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []), + app.completionsCompartment.of(app.settings.completions ? jsCompletions : []), editorSetup, toposTheme, app.chosenLanguage.of(javascript()), diff --git a/src/FileManagement.ts b/src/FileManagement.ts index 75db8a6..6b8b969 100644 --- a/src/FileManagement.ts +++ b/src/FileManagement.ts @@ -48,6 +48,7 @@ export interface Settings { * @param line_numbers - Whether or not to show line numbers * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips + * @param completions- Whether or not to show completions * @param send_clock - Whether or not to send midi clock * @param midi_channels_scripts - Whether midi input channels fires scripts * @param midi_clock_input - The name of the midi clock input @@ -64,6 +65,7 @@ export interface Settings { time_position: boolean; load_demo_songs: boolean; tips: boolean; + completions: boolean; send_clock: boolean; midi_channels_scripts: boolean; midi_clock_input: string | undefined; @@ -125,6 +127,7 @@ export class AppSettings { * @param line_numbers - Whether or not to show line numbers * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips + * @param completions - Whether or not to show completions * @param send_clock - Whether or not to send midi clock * @param midi_channels_scripts - Whether midi input channels fires scripts * @param midi_clock_input - The name of the midi clock input @@ -140,7 +143,8 @@ export class AppSettings { public selected_universe: string = "Default"; public line_numbers: boolean = true; public time_position: boolean = true; - public tips: boolean = true; + public tips: boolean = false; + public completions: boolean = false; public send_clock: boolean = false; public midi_channels_scripts: boolean = true; public midi_clock_input: string | undefined = undefined; @@ -164,6 +168,7 @@ export class AppSettings { this.line_numbers = settingsFromStorage.line_numbers; this.time_position = settingsFromStorage.time_position; this.tips = settingsFromStorage.tips; + this.completions = settingsFromStorage.completions; this.send_clock = settingsFromStorage.send_clock; this.midi_channels_scripts = settingsFromStorage.midi_channels_scripts; this.midi_clock_input = settingsFromStorage.midi_clock_input; @@ -193,6 +198,7 @@ export class AppSettings { line_numbers: this.line_numbers, time_position: this.time_position, tips: this.tips, + completions: this.completions, send_clock: this.send_clock, midi_channels_scripts: this.midi_channels_scripts, midi_clock_input: this.midi_clock_input, @@ -220,6 +226,7 @@ export class AppSettings { this.line_numbers = settings.line_numbers; this.time_position = settings.time_position; this.tips = settings.tips; + this.completions = settings.completions; this.send_clock = settings.send_clock; this.midi_channels_scripts = settings.midi_channels_scripts; this.midi_clock_input = settings.midi_clock_input; diff --git a/src/InterfaceLogic.ts b/src/InterfaceLogic.ts index 1cfd385..18bb17d 100644 --- a/src/InterfaceLogic.ts +++ b/src/InterfaceLogic.ts @@ -21,6 +21,7 @@ import { loadSamples } from "./API"; import { tryEvaluate } from "./Evaluator"; import { inlineHoveringTips } from "./documentation/inlineHelp"; import { lineNumbers } from "@codemirror/view"; +import { jsCompletions } from "./EditorSetup"; export const installInterfaceLogic = (app: Editor) => { (app.interface.line_numbers_checkbox as HTMLInputElement).checked = @@ -28,6 +29,8 @@ export const installInterfaceLogic = (app: Editor) => { (app.interface.time_position_checkbox as HTMLInputElement).checked = app.settings.time_position; (app.interface.tips_checkbox as HTMLInputElement).checked = app.settings.tips; + (app.interface.completion_checkbox as HTMLInputElement).checked = app.settings.completions; + (app.interface.midi_clock_checkbox as HTMLInputElement).checked = app.settings.send_clock; (app.interface.midi_channels_scripts as HTMLInputElement).checked = @@ -378,6 +381,18 @@ export const installInterfaceLogic = (app: Editor) => { }); }); + app.interface.completion_checkbox.addEventListener("change", () => { + let checked = (app.interface.completion_checkbox as HTMLInputElement).checked + ? true + : false; + app.settings.completions = checked; + app.view.dispatch({ + effects: app.completionsCompartment.reconfigure( + checked ? jsCompletions : [] + ), + }); + }); + app.interface.midi_clock_checkbox.addEventListener("change", () => { let checked = (app.interface.midi_clock_checkbox as HTMLInputElement) .checked diff --git a/src/Utils/Generic.ts b/src/Utils/Generic.ts new file mode 100644 index 0000000..d6e91e6 --- /dev/null +++ b/src/Utils/Generic.ts @@ -0,0 +1,71 @@ +/* + * Transforms object with arrays into array of objects + * + * @param {Record} input - Object with arrays + * @param {string[]} ignoredKeys - Keys to ignore + * @returns {Record[]} Array of objects + * + */ +export function objectWithArraysToArrayOfObjects(input: Record, ignoredKeys: string[]): Record[] { + ignoredKeys = ignoredKeys.map((k) => Array.isArray(input[k]) ? undefined : k).filter((k) => k !== undefined) as string[]; + const keys = Object.keys(input).filter((k) => !ignoredKeys.includes(k)); + const maxLength = Math.max( + ...keys.map((k) => + Array.isArray(input[k]) ? (input[k] as any[]).length : 1 + ) + ); + + const output: Record[] = []; + + for (let i = 0; i < maxLength; i++) { + const event: Record = {}; + for (const k of keys) { + if (ignoredKeys.includes(k)) { + event[k] = input[k]; + } else { + if (Array.isArray(input[k])) { + event[k] = (input[k] as any[])[i % (input[k] as any[]).length]; + } else { + event[k] = input[k]; + } + } + } + output.push(event); + } + return output; + }; + +/* + * Transforms array of objects into object with arrays + * + * @param {Record[]} array - Array of objects + * @param {Record} mergeObject - Object that is merged to each object in the array + * @returns {object} Merged object with arrays + * + */ +export function arrayOfObjectsToObjectWithArrays>(array: T[], mergeObject: Record = {}): Record { + return array.reduce((acc, obj) => { + Object.keys(mergeObject).forEach((key) => { + obj[key as keyof T] = mergeObject[key]; + }); + Object.keys(obj).forEach((key) => { + if (!acc[key as keyof T]) { + acc[key as keyof T] = []; + } + (acc[key as keyof T] as unknown[]).push(obj[key]); + }); + return acc; + }, {} as Record); + } + + /* + * Filter certain keys from object + * + * @param {Record} obj - Object to filter + * @param {string[]} filter - Keys to filter + * @returns {object} Filtered object + * + */ + export function filterObject(obj: Record, filter: string[]): Record { + return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key))); + } \ No newline at end of file diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts index 6eab352..f486ed8 100644 --- a/src/classes/AbstractEvents.ts +++ b/src/classes/AbstractEvents.ts @@ -2,9 +2,7 @@ import { type Editor } from "../main"; import { freqToMidi, resolvePitchBend, - getScale, - isScale, - parseScala, + safeScale } from "zifferjs"; export abstract class Event { @@ -204,8 +202,20 @@ export abstract class Event { return this.modify(func); }; - length = (value: number): Event => { - this.values["length"] = value; + noteLength = (value: number | number[], ...kwargs: number[]): Event => { + /** + * This function is used to set the note length of the Event. + */ + if(kwargs.length > 0) { + value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); + } + if(Array.isArray(value)) { + this.values["noteLength"] = value; + this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v*4*this.app.clock.ppqn)); + } else { + this.values["noteLength"] = value; + this.values.dur = this.app.clock.convertPulseToSecond(value*4*this.app.clock.ppqn); + } return this; }; } @@ -215,37 +225,101 @@ export abstract class AudibleEvent extends Event { super(app); } - octave = (value: number): this => { - this.values["octave"] = value; - this.update(); - return this; - }; - - key = (value: string): this => { - this.values["key"] = value; - this.update(); - return this; - }; - - scale = (value: string): this => { - if (!isScale(value)) { - this.values.parsedScale = parseScala(value) as number[]; - } else { - this.values.scaleName = value; - this.values.parsedScale = getScale(value) as number[]; + pitch = (value: number | number[], ...kwargs: number[]): this => { + /* + * This function is used to set the pitch of the Event. + * @param value - The pitch value + * @returns The Event + */ + if(kwargs.length > 0) { + value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); } - this.update(); + this.values["pitch"] = value; + if(this.values.key && this.values.parsedScale) this.update(); + return this; + } + + pc = this.pitch; + + octave = (value: number | number[], ...kwargs: number[]): this => { + /* + * This function is used to set the octave of the Event. + * @param value - The octave value + * @returns The Event + */ + if(kwargs.length > 0) { + value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); + } + this.values["octave"] = value; + if(this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update(); return this; }; - freq = (value: number): this => { + key = (value: string | string[], ...kwargs: string[]): this => { + /* + * This function is used to set the key of the Event. + * @param value - The key value + * @returns The Event + */ + if(kwargs.length > 0) { + value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); + } + this.values["key"] = value; + if((this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update(); + return this; + }; + + scale = (value: string | number | (number|string)[], ...kwargs: (string|number)[]): this => { + /* + * This function is used to set the scale of the Event. + * @param value - The scale value + * @returns The Event + */ + if(kwargs.length > 0) { + value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); + } + if (typeof value === "string" || typeof value === "number") { + this.values.parsedScale = safeScale(value) as number[]; + } else if(Array.isArray(value)) { + this.values.parsedScale = value.map((v) => safeScale(v)); + } + if(this.values.key && (this.values.pitch || this.values.pitch === 0)) { + this.update(); + } + return this; + }; + + freq = (value: number | number[], ...kwargs: number[]): this => { + /* + * This function is used to set the frequency of the Event. + * @param value - The frequency value + * @returns The Event + */ + if(kwargs.length > 0) { + value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); + } this.values["freq"] = value; - const midiNote = freqToMidi(value); - if (midiNote % 1 !== 0) { - this.values["note"] = Math.floor(midiNote); - this.values["bend"] = resolvePitchBend(midiNote)[1]; + if(Array.isArray(value)) { + this.values["note"] = []; + this.values["bend"] = []; + for(const v of value) { + const midiNote = freqToMidi(v); + if (midiNote % 1 !== 0) { + this.values["note"].push(Math.floor(midiNote)); + this.values["bend"].push(resolvePitchBend(midiNote)[1]); + } else { + this.values["note"].push(midiNote); + } + } + if(this.values.bend.length === 0) delete this.values.bend; } else { - this.values["note"] = midiNote; + const midiNote = freqToMidi(value); + if (midiNote % 1 !== 0) { + this.values["note"] = Math.floor(midiNote); + this.values["bend"] = resolvePitchBend(midiNote)[1]; + } else { + this.values["note"] = midiNote; + } } return this; }; diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index 1294d57..5fd0c5b 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -1,7 +1,8 @@ import { AudibleEvent } from "./AbstractEvents"; import { type Editor } from "../main"; import { MidiConnection } from "../IO/MidiConnection"; -import { midiToFreq, noteFromPc } from "zifferjs"; +import { noteFromPc, chord as parseChord } from "zifferjs"; +import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic"; export type MidiParams = { note: number; @@ -9,40 +10,49 @@ export type MidiParams = { channel?: number; port?: number; sustain?: number; + velocity?: number; } export class MidiEvent extends AudibleEvent { midiConnection: MidiConnection; - constructor(input: number | object, public app: Editor) { + constructor(input: MidiParams, public app: Editor) { super(app); - if (typeof input === "number") this.values["note"] = input; - else this.values = input; + this.values = input; this.midiConnection = app.api.MidiConnection; } - chord = (value: MidiParams[]): this => { - this.values["chord"] = value; - return this; + public chord = (value: string) => { + this.values.note = parseChord(value); + return this; }; - note = (value: number): this => { + note = (value: number | number[]): this => { this.values["note"] = value; return this; }; - sustain = (value: number): this => { + sustain = (value: number | number[]): this => { this.values["sustain"] = value; return this; }; - channel = (value: number): this => { + velocity = (value: number | number[]): this => { + this.values["velocity"] = value; + return this; + } + + channel = (value: number | number[]): this => { this.values["channel"] = value; return this; }; - port = (value: number | string): this => { - this.values["port"] = this.midiConnection.getMidiOutputIndex(value); + port = (value: number | string | number[] | string[]): this => { + if(typeof value === "string"){ + this.values["port"] = this.midiConnection.getMidiOutputIndex(value); + } else if(Array.isArray(value)){ + this.values["port"] = value.map((v) => typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v); + } return this; }; @@ -75,37 +85,46 @@ export class MidiEvent extends AudibleEvent { }; update = (): void => { - const [note, bend] = noteFromPc( - this.values.key || "C4", - this.values.pitch || 0, - this.values.parsedScale || "MAJOR", - this.values.octave || 0 - ); - this.values.note = note; - this.values.freq = midiToFreq(note); - if (bend) this.values.bend = bend; + // Get key, pitch, parsedScale and octave from this.values object + const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]); + + const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]); + + events.forEach((event) => { + const [note, bend] = noteFromPc( + event.key as number || "C4", + event.pitch as number || 0, + event.parsedScale as number[] || event.scale || "MAJOR", + event.octave as number || 0 + ); + event.note = note; + if(bend) event.bend = bend; + }); + + const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams; + + this.values.note = newArrays.note; + if(newArrays.bend) this.values.bend = newArrays.bend; }; out = (): void => { - function play(event: MidiEvent, params?: MidiParams): void { - const paramChannel = params && params.channel ? params.channel : 0; - const channel = event.values.channel ? event.values.channel : paramChannel; - const velocity = event.values.velocity ? event.values.velocity : 100; - const paramNote = params && params.note ? params.note : 60; - const note = event.values.note ? event.values.note : paramNote; + function play(event: MidiEvent, params: MidiParams): void { + const channel = params.channel ? params.channel : 0; + const velocity = params.velocity ? params.velocity : 100; + const note = params.note ? params.note : 60; - const sustain = event.values.sustain - ? event.values.sustain * + const sustain = params.sustain + ? params.sustain * event.app.clock.pulse_duration * event.app.api.ppqn() : event.app.clock.pulse_duration * event.app.api.ppqn(); - const bend = event.values.bend ? event.values.bend : undefined; - - const port = event.values.port - ? event.midiConnection.getMidiOutputIndex(event.values.port) - : event.midiConnection.getCurrentMidiPortIndex(); + const bend = params.bend ? params.bend : undefined; + const port = params.port + ? event.midiConnection.getMidiOutputIndex(params.port) + : event.midiConnection.getCurrentMidiPortIndex() || 0; + event.midiConnection.sendMidiNote( note, channel, @@ -116,13 +135,11 @@ export class MidiEvent extends AudibleEvent { ); } - if(this.values.chord) { - this.values.chord.forEach((p: MidiParams) => { - play(this, p); - }); - } else { - play(this); - } + const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[]; + + events.forEach((p: MidiParams) => { + play(this,p); + }); }; } diff --git a/src/classes/RestEvent.ts b/src/classes/RestEvent.ts index 4f4ec53..862d2d5 100644 --- a/src/classes/RestEvent.ts +++ b/src/classes/RestEvent.ts @@ -4,11 +4,11 @@ import { Event } from "./AbstractEvents"; export class RestEvent extends Event { constructor(length: number, app: Editor) { super(app); - this.values["length"] = length; + this.values["noteLength"] = length; } _fallbackMethod = (): Event => { - return RestEvent.createRestProxy(this.values["length"], this.app); + return RestEvent.createRestProxy(this.values["noteLength"], this.app); }; public static createRestProxy = ( diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 725458d..4156634 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -1,5 +1,10 @@ import { type Editor } from "../main"; import { AudibleEvent } from "./AbstractEvents"; +import { + filterObject, + arrayOfObjectsToObjectWithArrays, + objectWithArraysToArrayOfObjects, +} from "../Utils/Generic"; import { chord as parseChord, midiToFreq, @@ -13,328 +18,386 @@ import { } from "superdough"; export type SoundParams = { - dur: number; - s?: string; + dur: number | number[]; + s?: undefined | string | string[]; + n?: undefined | number | number[]; + analyze?: boolean; + note?: number | number[]; + freq?: number | number[]; + pitch?: number | number[]; + key?: string; + scale?: string; + parsedScale?: number[]; + octave?: number | number[]; }; export class SoundEvent extends AudibleEvent { nudge: number; + sound: any; - constructor(sound: string | object, public app: Editor) { + private methodMap = { + volume: ["volume", "vol"], + zrand: ["zrand", "zr"], + curve: ["curve"], + slide: ["slide", "sld"], + deltaSlide: ["deltaSlide", "dslide"], + pitchJump: ["pitchJump", "pj"], + pitchJumpTime: ["pitchJumpTime", "pjt"], + lfo: ["lfo"], + znoise: ["znoise"], + noise: ["noise"], + zmod: ["zmod"], + zcrush: ["zcrush"], + zdelay: ["zdelay"], + sustainVolume: ["sustainVolume"], + tremolo: ["tremolo"], + dur: ["dur"], + zzfx: ["zzfx"], + fmi: ["fmi"], + fmh: ["fmh"], + fmenv: ["fmenv"], + fmattack: ["fmattack", "fmatk"], + fmdecay: ["fmdecay", "fmdec"], + fmsustain: ["fmsustain", "fmsus"], + fmrelease: ["fmrelease", "fmrel"], + fmvelocity: ["fmvelocity", "fmvel"], + fmwave: ["fmwave", "fmw"], + fmadsr: (a: number, d: number, s: number, r: number) => { + this.updateValue("fmattack", a); + this.updateValue("fmdecay", d); + this.updateValue("fmsustain", s); + this.updateValue("fmrelease", r); + return this; + }, + fmad: (a: number, d: number) => { + this.updateValue("fmattack", a); + this.updateValue("fmdecay", d); + return this; + }, + ftype: ["ftype"], + fanchor: ["fanchor"], + attack: ["attack", "atk"], + decay: ["decay", "dec"], + sustain: ["sustain", "sus"], + release: ["release", "rel"], + adsr: (a: number, d: number, s: number, r: number) => { + this.updateValue("attack", a); + this.updateValue("decay", d); + this.updateValue("sustain", s); + this.updateValue("release", r); + return this; + }, + ad: (a: number, d: number) => { + this.updateValue("attack", a); + this.updateValue("decay", d); + this.updateValue("sustain", 0.0); + this.updateValue("release", 0.0); + return this; + }, + lpenv: ["lpenv", "lpe"], + lpattack: ["lpattack", "lpa"], + lpdecay: ["lpdecay", "lpd"], + lpsustain: ["lpsustain", "lps"], + lprelease: ["lprelease", "lpr"], + cutoff: (value: number, resonance?: number) => { + this.updateValue("cutoff", value); + if (resonance) { + this.updateValue("resonance", resonance); + } + return this; + }, + lpf: (value: number, resonance?: number) => { + this.updateValue("cutoff", value); + if (resonance) { + this.updateValue("resonance", resonance); + } + return this; + }, + resonance: (value: number) => { + if (value >= 0 && value <= 1) { + this.updateValue("resonance", 50 * value); + } + return this; + }, + lpadsr: (depth: number, a: number, d: number, s: number, r: number) => { + this.updateValue("lpenv", depth); + this.updateValue("lpattack", a); + this.updateValue("lpdecay", d); + this.updateValue("lpsustain", s); + this.updateValue("lprelease", r); + return this; + }, + lpad: (depth: number, a: number, d: number) => { + this.updateValue("lpenv", depth); + this.updateValue("lpattack", a); + this.updateValue("lpdecay", d); + this.updateValue("lpsustain", 0); + this.updateValue("lprelease", 0); + return this; + }, + hpenv: ["hpenv", "hpe"], + hpattack: ["hpattack", "hpa"], + hpdecay: ["hpdecay", "hpd"], + hpsustain: ["hpsustain", "hpsus"], + hprelease: ["hprelease", "hpr"], + hcutoff: (value: number, resonance?: number) => { + this.updateValue("hcutoff", value); + if (resonance) { + this.updateValue("hresonance", resonance); + } + return this; + }, + hpq: (value: number) => { + this.updateValue("hresonance", value); + return this; + }, + hpadsr: (depth: number, a: number, d: number, s: number, r: number) => { + this.updateValue("hpenv", depth); + this.updateValue("hpattack", a); + this.updateValue("hpdecay", d); + this.updateValue("hpsustain", s); + this.updateValue("hprelease", r); + return this; + }, + hpad: (depth: number, a: number, d: number) => { + this.updateValue("hpenv", depth); + this.updateValue("hpattack", a); + this.updateValue("hpdecay", d); + this.updateValue("hpsustain", 0); + this.updateValue("hprelease", 0); + return this; + }, + bpenv: ["bpenv", "bpe"], + bpattack: ["bpattack", "bpa"], + bpdecay: ["bpdecay", "bpd"], + bpsustain: ["bpsustain", "bps"], + bprelease: ["bprelease", "bpr"], + bandf: (value: number, resonance?: number) => { + this.updateValue("bandf", value); + if (resonance) { + this.updateValue("bandq", resonance); + } + return this; + }, + bpf: (value: number, resonance?: number) => { + this.updateValue("bandf", value); + if (resonance) { + this.updateValue("bandq", resonance); + } + return this; + }, + bandq: ["bandq", "bpq"], + bpadsr: (depth: number, a: number, d: number, s: number, r: number) => { + this.updateValue("bpenv", depth); + this.updateValue("bpattack", a); + this.updateValue("bpdecay", d); + this.updateValue("bpsustain", s); + this.updateValue("bprelease", r); + return this; + }, + bpad: (depth: number, a: number, d: number) => { + this.updateValue("bpenv", depth); + this.updateValue("bpattack", a); + this.updateValue("bpdecay", d); + this.updateValue("bpsustain", 0); + this.updateValue("bprelease", 0); + return this; + }, + vib: ["vib"], + vibmod: ["vibmod"], + fm: (value: number | string) => { + if (typeof value === "number") { + this.values["fmi"] = value; + } else { + let values = value.split(":"); + this.values["fmi"] = parseFloat(values[0]); + if (values.length > 1) this.values["fmh"] = parseFloat(values[1]); + } + return this; + }, + loop: ["loop"], + loopBegin: ["loopBegin", "loopb"], + loopEnd: ["loopEnd", "loope"], + begin: ["begin"], + end: ["end"], + gain: ["gain"], + dbgain: (value: number) => { + this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + return this; + }, + db: (value: number) => { + this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + return this; + }, + velocity: ["velocity", "vel"], + pan: ["pan"], + cut: ["cut"], + clip: ["clip"], + n: ["n"], + speed: ["speed", "spd"], + coarse: ["coarse"], + crush: ["crush"], + shape: ["shape"], + vowel: ["vowel", "vow"], + delay: ["delay", "del"], + delayfeedback: ["delayfeedback", "delayfb"], + delaytime: ["delaytime", "delayt"], + orbit: ["orbit", "o"], + room: ["room", "rm"], + roomfade: ["roomfade", "rfade"], + roomlp: ["roomlp", "rlp"], + roomdim: ["roomdim", "rdim"], + sound: ["sound", "s"], + size: (value: number) => { + this.updateValue("roomsize", value); + return this; + }, + sz: (value: number) => { + this.updateValue("roomsize", value); + return this; + }, + comp: ["compressor", "cmp"], + ratio: (value: number) => { + this.updateValue("compressorRatio", value); + return this; + }, + knee: (value: number) => { + this.updateValue("compressorKnee", value); + return this; + }, + compAttack: (value: number) => { + this.updateValue("compressorAttack", value); + return this; + }, + compRelease: (value: number) => { + this.updateValue("compressorRelease", value); + return this; + }, + stretch: (beat: number) => { + this.updateValue("unit", "c"); + this.updateValue("speed", 1 / beat); + this.updateValue("cut", beat); + return this; + }, + }; + + constructor(sound: string | string[] | SoundParams, public app: Editor) { super(app); this.nudge = app.dough_nudge / 100; - if (typeof sound === "string") { + + for (const [methodName, keys] of Object.entries(this.methodMap)) { + if (Symbol.iterator in Object(keys)) { + for (const key of keys as string[]) { + // @ts-ignore + this[key] = (value: number) => this.updateValue(keys[0], value); + } + } else { + // @ts-ignore + this[methodName] = keys; + } + } + this.values = this.processSound(sound); + } + + private processSound = ( + sound: string | string[] | SoundParams | SoundParams[] + ): SoundParams => { + if (Array.isArray(sound) && typeof sound[0] === "string") { + const s: string[] = []; + const n: number[] = []; + sound.forEach((str) => { + const parts = (str as string).split(":"); + s.push(parts[0]); + if (parts[1]) { + n.push(parseInt(parts[1])); + } + }); + return { + s, + n: n.length > 0 ? n : undefined, + dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), + analyze: true, + }; + } else if (typeof sound === "object") { + const validatedObj: SoundParams = { + dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), + analyze: true, + ...(sound as Partial), + }; + return validatedObj; + } else { if (sound.includes(":")) { - this.values = { - s: sound.split(":")[0], - n: sound.split(":")[1], - dur: app.clock.convertPulseToSecond(app.clock.ppqn), + const vals = sound.split(":"); + const s = vals[0]; + const n = parseInt(vals[1]); + return { + s, + n, + dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), analyze: true, }; } else { - this.values = { s: sound, dur: 0.5, analyze: true }; + return { s: sound, dur: 0.5, analyze: true }; } - } else { - this.values = sound; } - } + }; - private updateValue(key: string, value: T): this { + private updateValue( + key: string, + value: T | T[] | SoundParams[] | null + ): this { + if (value == null) return this; this.values[key] = value; return this; } // ================================================================================ - // ZZFX Sound Parameters + // AbstactEvent overrides // ================================================================================ - public volume = (value: number) => this.updateValue("volume", value); - public vol = this.volume; - public zrand = (value: number) => this.updateValue("zrand", value); - public curve = (value: number) => this.updateValue("curve", value); - public slide = (value: number) => this.updateValue("slide", value); - public sld = this.slide; - public deltaSlide = (value: number) => this.updateValue("deltaSlide", value); - public dslide = this.deltaSlide; - public pitchJump = (value: number) => this.updateValue("pitchJump", value); - public pj = this.pitchJump; - public pitchJumpTime = (value: number) => - this.updateValue("pitchJumpTime", value); - public pjt = this.pitchJumpTime; - public lfo = (value: number) => this.updateValue("lfo", value); - public znoise = (value: number) => this.updateValue("znoise", value); - public noise = (value: number) => this.updateValue("noise", value); - public zmod = (value: number) => this.updateValue("zmod", value); - public zcrush = (value: number) => this.updateValue("zcrush", value); - public zdelay = (value: number) => this.updateValue("zdelay", value); - public sustainVolume = (value: number) => - this.updateValue("sustainVolume", value); - public tremolo = (value: number) => this.updateValue("tremolo", value); - public dur = (value: number) => this.updateValue("dur", value); - public zzfx = (value: number[]) => this.updateValue("zzfx", value); - - // ================================================================================ - // Basic Audio Engine Parameters - // ================================================================================ - - // FM Synthesis - public fmi = (value: number) => this.updateValue("fmi", value); - public fmh = (value: number) => this.updateValue("fmh", value); - public fmenv = (value: "lin" | "exp") => this.updateValue("fmenv", value); - public fmattack = (value: number) => this.updateValue("fmattack", value); - public fmatk = this.fmattack; - public fmdecay = (value: number) => this.updateValue("fmdecay", value); - public fmdec = this.fmdecay; - public fmsustain = (value: number) => this.updateValue("fmsustain", value); - public fmsus = this.fmsustain; - public fmrelease = (value: number) => this.updateValue("fmrelease", value); - public fmrel = this.fmrelease; - public fmvelocity = (value: number) => this.updateValue("fmvelocity", value); - public fmvel = this.fmvelocity; - public fmwave = (value: "sine" | "triangle" | "sawtooth" | "square") => - this.updateValue("fmwave", value); - public fmw = this.fmwave; - - // Filter type - public ftype = (value: "12db" | "24db") => this.updateValue("ftype", value); - public fanchor = (value: number) => this.updateValue("fanchor", value); - - // Amplitude Envelope - public attack = (value: number) => this.updateValue("attack", value); - public atk = this.attack; - public decay = (value: number) => this.updateValue("decay", value); - public dec = this.decay; - public sustain = (value: number) => this.updateValue("sustain", value); - public sus = this.sustain; - public release = (value: number) => this.updateValue("release", value); - public rel = this.release; - public adsr = (a: number, d: number, s: number, r: number) => { - this.attack(a); - this.decay(d); - this.sustain(s); - this.release(r); - return this; - }; - public ad = (a: number, d: number) => { - this.attack(a); - this.decay(d); - this.sustain(0.0); - this.release(0.0); - return this; - }; - - // Lowpass filter - public lpenv = (value: number) => this.updateValue("lpenv", value); - public lpe = (value: number) => this.updateValue("lpenv", value); - public lpattack = (value: number) => this.updateValue("lpattack", value); - public lpa = this.lpattack; - public lpdecay = (value: number) => this.updateValue("lpdecay", value); - public lpd = this.lpdecay; - public lpsustain = (value: number) => this.updateValue("lpsustain", value); - public lps = this.lpsustain; - public lprelease = (value: number) => this.updateValue("lprelease", value); - public lpr = this.lprelease; - public cutoff = (value: number, resonance?: number) => { - this.updateValue("cutoff", value); - if (resonance) { - this.resonance(resonance) + modify = (func: Function): this => { + const funcResult = func(this); + if (funcResult instanceof Object) return funcResult; + else { + func(this.values); + this.update(); + return this; } - return this; - } - public lpf = this.cutoff; - public resonance = (value: number) => { - if (value >= 0 && value <= 1) { - this.updateValue( - "resonance", - 50 * value + }; + + update = (): void => { + const filteredValues = filterObject(this.values, [ + "key", + "pitch", + "parsedScale", + "octave", + ]); + const events = objectWithArraysToArrayOfObjects(filteredValues, [ + "parsedScale", + ]); + + events.forEach((event) => { + const [note, _] = noteFromPc( + (event.key as number) || "C4", + (event.pitch as number) || 0, + (event.parsedScale as number[]) || event.scale || "MAJOR", + (event.octave as number) || 0 ); - } - return this; - } - public lpq = this.resonance; - public lpadsr = ( - depth: number, - a: number, - d: number, - s: number, - r: number - ) => { - this.lpenv(depth); - this.lpattack(a); - this.lpdecay(d); - this.lpsustain(s); - this.lprelease(r); - return this; - }; - public lpad = ( - depth: number, - a: number, - d: number, - ) => { - this.lpenv(depth); - this.lpattack(a); - this.lpdecay(d); - this.lpsustain(0); - this.lprelease(0); - return this; + event.note = note; + event.freq = midiToFreq(note); + }); + + const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams; + + this.values.note = newArrays.note; + this.values.freq = newArrays.freq; }; - - // Highpass filter - - public hpenv = (value: number) => this.updateValue("hpenv", value); - public hpe = (value: number) => this.updateValue("hpe", value); - public hpattack = (value: number) => this.updateValue("hpattack", value); - public hpa = this.hpattack; - public hpdecay = (value: number) => this.updateValue("hpdecay", value); - public hpd = this.hpdecay; - public hpsustain = (value: number) => this.updateValue("hpsustain", value); - public hpsus = this.hpsustain; - public hprelease = (value: number) => this.updateValue("hprelease", value); - public hpr = this.hprelease; - public hcutoff = (value: number) => this.updateValue("hcutoff", value); - public hpf = this.hcutoff; - public hresonance = (value: number, resonance?: number) => { - this.updateValue("hresonance", value); - if (resonance) { - this.resonance(resonance) - } - return this; - } - public hpq = this.hresonance; - public hpadsr = ( - depth: number, - a: number, - d: number, - s: number, - r: number - ) => { - this.hpenv(depth); - this.hpattack(a); - this.hpdecay(d); - this.hpsustain(s); - this.hprelease(r); - return this; - }; - public hpad = ( - depth: number, - a: number, - d: number, - ) => { - this.hpenv(depth); - this.hpattack(a); - this.hpdecay(d); - this.hpsustain(0); - this.hprelease(0); - return this; + public chord = (value: string) => { + const chord = parseChord(value); + return this.updateValue("note", chord); }; - // Bandpass filter - - public bpenv = (value: number) => this.updateValue("bpenv", value); - public bpe = (value: number) => this.updateValue("bpe", value); - public bpattack = (value: number) => this.updateValue("bpattack", value); - public bpa = this.bpattack; - public bpdecay = (value: number) => this.updateValue("bpdecay", value); - public bpd = this.bpdecay; - public bpsustain = (value: number) => this.updateValue("bpsustain", value); - public bps = this.bpsustain; - public bprelease = (value: number) => this.updateValue("bprelease", value); - public bpr = this.bprelease; - public bandf = (value: number, resonance?: number) => { - this.updateValue("bandf", value); - if (resonance) { - this.resonance(resonance) - } - return this; - } - public bpf = this.bandf; - public bandq = (value: number) => this.updateValue("bandq", value); - public bpq = this.bandq; - public bpadsr = ( - depth: number, - a: number, - d: number, - s: number, - r: number - ) => { - this.bpenv(depth); - this.bpattack(a); - this.bpdecay(d); - this.bpsustain(s); - this.bprelease(r); - return this; - }; - public bpad = ( - depth: number, - a: number, - d: number, - ) => { - this.bpenv(depth); - this.bpattack(a); - this.bpdecay(d); - this.bpsustain(0); - this.bprelease(0); - return this; - }; - - - public freq = (value: number) => this.updateValue("freq", value); - public f = this.freq; - public vib = (value: number) => this.updateValue("vib", value); - public vibmod = (value: number) => this.updateValue("vibmod", value); - public fm = (value: number | string) => { - if (typeof value === "number") { - this.values["fmi"] = value; - } else { - let values = value.split(":"); - this.values["fmi"] = parseFloat(values[0]); - if (values.length > 1) this.values["fmh"] = parseFloat(values[1]); - } - return this; - }; - - // Sampler looping - public loop = (value: number) => this.updateValue("loop", value); - public loopBegin = (value: number) => this.updateValue("loopBegin", value); - public loopEnd = (value: number) => this.updateValue("loopEnd", value); - public begin = (value: number) => this.updateValue("begin", value); - public end = (value: number) => this.updateValue("end", value); - - // Gain management - public gain = (value: number) => this.updateValue("gain", value); - public dbgain = (value: number) => - this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); - public db = this.dbgain; - public velocity = (value: number) => this.updateValue("velocity", value); - public vel = this.velocity; - - // Panoramic control (stereo) - public pan = (value: number) => this.updateValue("pan", value); - - // Frequency management - - public sound = (value: string) => this.updateValue("s", value); - public chord = ( - value: string | object[] | number[] | number, - ...kwargs: number[] - ) => { - if (typeof value === "string") { - const chord = parseChord(value); - value = chord.map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - } else if (value instanceof Array && typeof value[0] === "number") { - value = (value as number[]).map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - } else if (typeof value === "number" && kwargs.length > 0) { - value = [value, ...kwargs].map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - } - return this.updateValue("chord", value); - }; public invert = (howMany: number = 0) => { if (this.values.chord) { let notes = this.values.chord.map( @@ -352,10 +415,6 @@ export class SoundEvent extends AudibleEvent { return this; } }; - public snd = this.sound; - public cut = (value: number) => this.updateValue("cut", value); - public clip = (value: number) => this.updateValue("clip", value); - public n = (value: number) => this.updateValue("n", value); public note = (value: number | string | null) => { if (typeof value === "string") { return this.updateValue("note", noteNameToMidi(value)); @@ -365,109 +424,13 @@ export class SoundEvent extends AudibleEvent { return this.updateValue("note", value); } }; - public speed = (value: number) => this.updateValue("speed", value); - public spd = this.speed; - - // Creative sampler effects - public coarse = (value: number) => this.updateValue("coarse", value); - public crush = (value: number) => this.updateValue("crush", value); - public shape = (value: number) => this.updateValue("shape", value); - public vowel = (value: number) => this.updateValue("vowel", value); - public vow = this.vowel; - - // Delay control - public delay = (value: number) => this.updateValue("delay", value); - public del = this.delay; - public delayfeedback = (value: number) => - this.updateValue("delayfeedback", value); - public delayfb = this.delayfeedback; - public delaytime = (value: number) => this.updateValue("delaytime", value); - public delayt = this.delaytime; - - // Orbit management - public orbit = (value: number) => this.updateValue("orbit", value); - public o = this.orbit; - - // Reverb management - public room = (value: number) => this.updateValue("room", value); - public rm = this.room; - public roomfade = (value: number) => this.updateValue("roomfade", value); - public rfade = this.roomfade; - public roomlp = (value: number) => this.updateValue("roomlp", value); - public rlp = this.roomlp; - public roomdim = (value: number) => this.updateValue("roomdim", value); - public rdim = this.roomdim; - public size = (value: number) => this.updateValue("roomsize", value); - public sz = this.size; - public rev = (room: number, size: number, fade?: number, lp?: number, dim?: number) => { - this.updateValue("room", room) - this.updateValue("roomsize", size) - if (fade) - this.updateValue("roomfade", fade) - if (lp) - this.updateValue("roomlp", lp) - if (dim) - this.updateValue("roomdim", dim) - - return this; - } - - // Compressor - public comp = (value: number) => this.updateValue("compressor", value); - public cmp = this.comp; - public ratio = (value: number) => this.updateValue("compressorRatio", value); - public rt = this.ratio; - public knee = (value: number) => this.updateValue("compressorKnee", value); - public kn = this.knee; - public compAttack = (value: number) => - this.updateValue("compressorAttack", value); - public cmpa = this.compAttack; - public compRelease = (value: number) => - this.updateValue("compressorRelease", value); - public cmpr = this.compRelease; - - // Unit - public stretch = (beat: number) => { - this.updateValue("unit", "c"); - this.updateValue("speed", 1 / beat); - this.updateValue("cut", beat); - return this; - }; - - // ================================================================================ - // AbstactEvent overrides - // ================================================================================ - - modify = (func: Function): this => { - const funcResult = func(this); - if (funcResult instanceof Object) return funcResult; - else { - func(this.values); - this.update(); - return this; - } - }; - - update = (): void => { - const [note, _] = noteFromPc( - this.values.key || "C4", - this.values.pitch || 0, - this.values.parsedScale || "MAJOR", - this.values.octave || 0 - ); - this.values.freq = midiToFreq(note); - }; out = (): void => { - console.log(this.app.clock.deviation) - if (this.values.chord) { - this.values.chord.forEach((obj: { [key: string]: number }) => { - const copy = { ...this.values }; - copy.freq = obj.freq; - superdough(copy, this.nudge - this.app.clock.deviation, this.values.dur); - }); - } else { - superdough(this.values, this.nudge - this.app.clock.deviation, this.values.dur); + const events = objectWithArraysToArrayOfObjects(this.values, [ + "parsedScale", + ]); + for (const event of events) { + superdough(event, this.nudge, event.dur); } }; } diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index 4a66e4f..43ea493 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -5,6 +5,7 @@ import { SkipEvent } from "./SkipEvent"; import { SoundEvent, SoundParams } from "./SoundEvent"; import { MidiEvent, MidiParams } from "./MidiEvent"; import { RestEvent } from "./RestEvent"; +import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic"; export type InputOptions = { [key: string]: string | number }; @@ -23,7 +24,12 @@ export class Player extends Event { options: InputOptions = {}; skipIndex = 0; - constructor(input: string, options: InputOptions, public app: Editor, zid: string = "") { + constructor( + input: string, + options: InputOptions, + public app: Editor, + zid: string = "" + ) { super(app); this.input = input; this.options = options; @@ -108,15 +114,17 @@ export class Player extends Event { this.app.api.resetAllFromCache(); } - const patternIsStarting = (this.notStarted() && + const patternIsStarting = + this.notStarted() && (this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) && - this.origin() >= this.waitTime); + this.origin() >= this.waitTime; - const timeToPlayNext = (this.current && + const timeToPlayNext = + this.current && this.pulseToSecond(this.origin()) >= - this.pulseToSecond(this.lastCallTime) + - this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) && - this.origin() >= this.waitTime); + this.pulseToSecond(this.lastCallTime) + + this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) && + this.origin() >= this.waitTime; // If pattern is starting or it's time to play next event const areWeThereYet = patternIsStarting || timeToPlayNext; @@ -139,37 +147,44 @@ export class Player extends Event { }; sound(name?: string) { - if (this.areWeThereYet()) { const event = this.next() as Pitch | Chord | ZRest; - const noteLengthInSeconds = this.app.clock.convertPulseToSecond(event.duration * 4 * this.app.clock.ppqn); + const noteLengthInSeconds = this.app.clock.convertPulseToSecond( + event.duration * 4 * this.app.clock.ppqn + ); if (event instanceof Pitch) { const obj = event.getExisting( "freq", + "note", "pitch", "key", "scale", "octave", "parsedScale" - ); + ) as SoundParams; if (event.sound) name = event.sound as string; - if (event.soundIndex) obj.n = event.soundIndex; + if (event.soundIndex) obj.n = event.soundIndex as number; obj.dur = noteLengthInSeconds; return new SoundEvent(obj, this.app).sound(name || "sine"); } else if (event instanceof Chord) { const pitches = event.pitches.map((p) => { return p.getExisting( "freq", + "note", "pitch", "key", "scale", "octave", "parsedScale" ); - }); - const sound: SoundParams = { dur: noteLengthInSeconds }; - if (name) sound.s = name; - return new SoundEvent(sound, this.app).chord(pitches); + }) as SoundParams[]; + const add = { dur: noteLengthInSeconds } as SoundParams; + if (name) add.s = name; + let sound = arrayOfObjectsToObjectWithArrays( + pitches, + add + ) as SoundParams; + return new SoundEvent(sound, this.app); } else if (event instanceof ZRest) { return RestEvent.createRestProxy(event.duration, this.app); } @@ -188,17 +203,18 @@ export class Player extends Event { "key", "scale", "octave", - "parsedScale", - ); + "parsedScale" + ) as MidiParams; if (event instanceof Pitch) { - if (event.soundIndex) obj.channel = event.soundIndex; + if (event.soundIndex) obj.channel = event.soundIndex as number; const note = new MidiEvent(obj, this.app); return value ? note.note(value) : note; } else if (event instanceof ZRest) { return RestEvent.createRestProxy(event.duration, this.app); } else if (event instanceof Chord) { const pitches = event.midiChord() as MidiParams[]; - return new MidiEvent(obj, this.app).chord(pitches); + const obj = arrayOfObjectsToObjectWithArrays(pitches) as MidiParams; + return new MidiEvent(obj, this.app); } } else { return SkipEvent.createSkipProxy(); @@ -232,7 +248,7 @@ export class Player extends Event { this.ziffers.invert(n); } return this; - } + }; retrograde() { if (this.atTheBeginning()) this.ziffers.retrograde(); diff --git a/src/documentation/inlineHelp.ts b/src/documentation/inlineHelp.ts index ef6bc24..eddb2c3 100644 --- a/src/documentation/inlineHelp.ts +++ b/src/documentation/inlineHelp.ts @@ -1,5 +1,7 @@ import { hoverTooltip } from "@codemirror/view"; import { type EditorView } from "@codemirror/view"; +import { CompletionContext } from "@codemirror/autocomplete" + interface InlineCompletion { name: string; @@ -23,7 +25,7 @@ const completionDatabase: CompletionDatabase = { name: "delayr", category: "time", description: "Delay a function n times by t ms", - example: "delayr(50, 3, () => beat(1) :: log('delayed'))", + example: "delayr(50,3,()=> beat(1)::log('hey!'))", }, toss: { name: "toss", @@ -35,7 +37,7 @@ const completionDatabase: CompletionDatabase = { name: "lpadsr", category: "synthesis", description: "Lowpass filter ADSR envelope", - example: "sound('sawtooth').lpadsr(2, 0, .1, 0, 0).out()", + example: "sound('sawtooth').lpadsr(2,0,.1,0,0).out()", }, lpenv: { name: "lpenv", @@ -968,3 +970,29 @@ export const inlineHoveringTips = hoverTooltip( }; } ); + +export const toposCompletions = (context: CompletionContext) => { + let word = context.matchBefore(/\w*/) + if (word) { + if (word.from == word.to && !context.explicit) + return null + return { + from: word.from, + options: Object.keys(completionDatabase).map((key) => ({ + label: key, + type: completionDatabase[key].category, + info: () => { + let div = document.createElement('div'); + div.innerHTML = ` +

${completionDatabase[key].name} [${completionDatabase[key].category}]

+

${completionDatabase[key].description}

+
${completionDatabase[key].example}
+ ` + div.classList.add("px-4", "py-2", "rounded-lg", "w-92"); + return div + } + })) + } + } +} + diff --git a/src/main.ts b/src/main.ts index d1c8203..60c8c7e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,6 +42,7 @@ export class Editor { withLineNumbers!: Compartment; vimModeCompartment!: Compartment; hoveringCompartment!: Compartment; + completionsCompartment!: Compartment; chosenLanguage!: Compartment; dynamicPlugins!: Compartment; currentDocumentationPane: string = "introduction"; @@ -64,7 +65,7 @@ export class Editor { color: "#fdba74", thickness: 4, refresh: 1, - fftSize: 256, + fftSize: 1024, orientation: "horizontal", is3D: false, size: 1,