-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.
-
+
## 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 = `
+