Merge branch 'main' into clockwork

This commit is contained in:
2023-11-03 19:07:52 +01:00
17 changed files with 760 additions and 540 deletions

View File

@ -14,9 +14,9 @@
</p> </p>
</p> </p>
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 ## Disclaimer

BIN
img/topos_gif.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

View File

@ -261,6 +261,10 @@
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"> <input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Hovering Tips</label> <label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Hovering Tips</label>
</div> </div>
<div class="flex items-center mb-4 ml-5">
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Completions</label>
</div>
<div class="flex items-center mb-4 ml-5"> <div class="flex items-center mb-4 ml-5">
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"> <input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Load Demo Song</label> <label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Load Demo Song</label>

View File

@ -9,7 +9,7 @@ import { tryEvaluate, evaluateOnce } from "./Evaluator";
import { DrunkWalk } from "./Utils/Drunk"; import { DrunkWalk } from "./Utils/Drunk";
import { Editor } from "./main"; import { Editor } from "./main";
import { SoundEvent } from "./classes/SoundEvent"; import { SoundEvent } from "./classes/SoundEvent";
import { MidiEvent } from "./classes/MidiEvent"; import { MidiEvent, MidiParams } from "./classes/MidiEvent";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
import { InputOptions, Player } from "./classes/ZPlayer"; import { InputOptions, Player } from "./classes/ZPlayer";
import { import {
@ -28,6 +28,7 @@ import {
import { Speaker } from "./StringExtensions"; import { Speaker } from "./StringExtensions";
import { getScaleNotes } from "zifferjs"; import { getScaleNotes } from "zifferjs";
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
import { SkipEvent } from './classes/SkipEvent';
interface ControlChange { interface ControlChange {
channel: number; channel: number;
@ -390,9 +391,10 @@ export class UserAPI {
}; };
public midi = ( public midi = (
value: number | object = 60, value: number | number[] = 60,
velocity?: number, velocity?: number | number[],
channel?: number channel?: number | number[],
port?: number | string | number[] | string[]
): MidiEvent => { ): MidiEvent => {
/** /**
* Sends a MIDI note to the current MIDI output. * Sends a MIDI note to the current MIDI output.
@ -402,24 +404,9 @@ export class UserAPI {
* { channel: 0, velocity: 100, duration: 0.5 } * { channel: 0, velocity: 100, duration: 0.5 }
*/ */
if (velocity !== undefined) { const event = {note: value, velocity, channel, port} as MidiParams
// Check if value is of type number
if (typeof value === "number") {
value = { note: value };
}
// @ts-ignore
value["velocity"] = velocity;
}
if (channel !== undefined) { return new MidiEvent(event, this.app);
if (typeof value === "number") {
value = { note: value };
}
// @ts-ignore
value["channel"] = channel;
}
return new MidiEvent(value, this.app);
}; };
public sysex = (data: Array<number>): void => { public sysex = (data: Array<number>): void => {
@ -1893,8 +1880,9 @@ export class UserAPI {
// Trivial functions // Trivial functions
// ============================================================= // =============================================================
sound = (sound: string | object) => { sound = (sound: string | string[] | null | undefined) => {
return new SoundEvent(sound, this.app); if(sound) return new SoundEvent(sound, this.app);
else return new SkipEvent();
}; };
snd = this.sound; snd = this.sound;

View File

@ -125,6 +125,8 @@ export interface OscilloscopeConfig {
size: number; size: number;
} }
let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg'
/** /**
* Initializes and runs an oscilloscope using an AnalyzerNode. * Initializes and runs an oscilloscope using an AnalyzerNode.
* @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope. * @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope.
@ -163,6 +165,8 @@ export const runOscilloscope = (
} }
analyzer.getFloatTimeDomainData(dataArray); analyzer.getFloatTimeDomainData(dataArray);
canvasCtx.globalCompositeOperation = 'source-over';
canvasCtx.fillStyle = "rgba(0, 0, 0, 0)"; canvasCtx.fillStyle = "rgba(0, 0, 0, 0)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
@ -179,40 +183,65 @@ export const runOscilloscope = (
} else { } else {
canvasCtx.strokeStyle = app.osc.color; 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(); 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) { 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 x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4;
const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 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") { } else if (app.osc.orientation === "horizontal") {
let x = 0;
const sliceWidth = (WIDTH * 1.0) / dataArray.length; const sliceWidth = (WIDTH * 1.0) / dataArray.length;
const yOffset = HEIGHT / 4; 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 v = dataArray[i] * 0.5 * HEIGHT * app.osc.size;
const y = v + yOffset; 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; x += sliceWidth;
} }
canvasCtx.lineTo(WIDTH, yOffset); canvasCtx.lineTo(WIDTH, yOffset);
} else { } else {
let y = 0;
const sliceHeight = (HEIGHT * 1.0) / dataArray.length; const sliceHeight = (HEIGHT * 1.0) / dataArray.length;
const xOffset = WIDTH / 4; 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 v = dataArray[i] * 0.5 * WIDTH * app.osc.size;
const x = v + xOffset; 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; y += sliceHeight;
} }
canvasCtx.lineTo(xOffset, HEIGHT); canvasCtx.lineTo(xOffset, HEIGHT);
} }
canvasCtx.stroke(); canvasCtx.stroke();
canvasCtx.globalAlpha = 1.0;
} }
draw(); draw();
}; };

View File

@ -1,12 +1,12 @@
export type ElementMap = { export type ElementMap = {
[key: string]: [key: string]:
| HTMLElement | HTMLElement
| HTMLButtonElement | HTMLButtonElement
| HTMLDivElement | HTMLDivElement
| HTMLInputElement | HTMLInputElement
| HTMLSelectElement | HTMLSelectElement
| HTMLCanvasElement | HTMLCanvasElement
| HTMLFormElement; | HTMLFormElement;
}; };
export const singleElements = { export const singleElements = {
@ -36,6 +36,7 @@ export const singleElements = {
line_numbers_checkbox: "show-line-numbers", line_numbers_checkbox: "show-line-numbers",
time_position_checkbox: "show-time-position", time_position_checkbox: "show-time-position",
tips_checkbox: "show-tips", tips_checkbox: "show-tips",
completion_checkbox: "show-completions",
midi_clock_checkbox: "send-midi-clock", midi_clock_checkbox: "send-midi-clock",
midi_channels_scripts: "midi-channels-scripts", midi_channels_scripts: "midi-channels-scripts",
midi_clock_ppqn: "midi-clock-ppqn-input", midi_clock_ppqn: "midi-clock-ppqn-input",

View File

@ -34,6 +34,12 @@ import { EditorView } from "codemirror";
import { toposTheme } from "./themes/toposTheme"; import { toposTheme } from "./themes/toposTheme";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { inlineHoveringTips } from "./documentation/inlineHelp"; 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 = (() => [ export const editorSetup: Extension = (() => [
highlightActiveLineGutter(), highlightActiveLineGutter(),
@ -47,8 +53,6 @@ export const editorSetup: Extension = (() => [
bracketMatching(), bracketMatching(),
closeBrackets(), closeBrackets(),
autocompletion(), autocompletion(),
// rectangularSelection(),
// crosshairCursor(),
highlightActiveLine(), highlightActiveLine(),
highlightSelectionMatches(), highlightSelectionMatches(),
keymap.of([ keymap.of([
@ -62,6 +66,7 @@ export const editorSetup: Extension = (() => [
export const installEditor = (app: Editor) => { export const installEditor = (app: Editor) => {
app.vimModeCompartment = new Compartment(); app.vimModeCompartment = new Compartment();
app.hoveringCompartment = new Compartment(); app.hoveringCompartment = new Compartment();
app.completionsCompartment = new Compartment();
app.withLineNumbers = new Compartment(); app.withLineNumbers = new Compartment();
app.chosenLanguage = new Compartment(); app.chosenLanguage = new Compartment();
app.fontSize = new Compartment(); app.fontSize = new Compartment();
@ -86,6 +91,7 @@ export const installEditor = (app: Editor) => {
app.withLineNumbers.of(lines), app.withLineNumbers.of(lines),
app.fontSize.of(fontModif), app.fontSize.of(fontModif),
app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []), app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []),
app.completionsCompartment.of(app.settings.completions ? jsCompletions : []),
editorSetup, editorSetup,
toposTheme, toposTheme,
app.chosenLanguage.of(javascript()), app.chosenLanguage.of(javascript()),

View File

@ -48,6 +48,7 @@ export interface Settings {
* @param line_numbers - Whether or not to show line numbers * @param line_numbers - Whether or not to show line numbers
* @param time_position - Whether or not to show time position * @param time_position - Whether or not to show time position
* @param tips - Whether or not to show tips * @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 send_clock - Whether or not to send midi clock
* @param midi_channels_scripts - Whether midi input channels fires scripts * @param midi_channels_scripts - Whether midi input channels fires scripts
* @param midi_clock_input - The name of the midi clock input * @param midi_clock_input - The name of the midi clock input
@ -64,6 +65,7 @@ export interface Settings {
time_position: boolean; time_position: boolean;
load_demo_songs: boolean; load_demo_songs: boolean;
tips: boolean; tips: boolean;
completions: boolean;
send_clock: boolean; send_clock: boolean;
midi_channels_scripts: boolean; midi_channels_scripts: boolean;
midi_clock_input: string | undefined; midi_clock_input: string | undefined;
@ -125,6 +127,7 @@ export class AppSettings {
* @param line_numbers - Whether or not to show line numbers * @param line_numbers - Whether or not to show line numbers
* @param time_position - Whether or not to show time position * @param time_position - Whether or not to show time position
* @param tips - Whether or not to show tips * @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 send_clock - Whether or not to send midi clock
* @param midi_channels_scripts - Whether midi input channels fires scripts * @param midi_channels_scripts - Whether midi input channels fires scripts
* @param midi_clock_input - The name of the midi clock input * @param midi_clock_input - The name of the midi clock input
@ -140,7 +143,8 @@ export class AppSettings {
public selected_universe: string = "Default"; public selected_universe: string = "Default";
public line_numbers: boolean = true; public line_numbers: boolean = true;
public time_position: 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 send_clock: boolean = false;
public midi_channels_scripts: boolean = true; public midi_channels_scripts: boolean = true;
public midi_clock_input: string | undefined = undefined; public midi_clock_input: string | undefined = undefined;
@ -164,6 +168,7 @@ export class AppSettings {
this.line_numbers = settingsFromStorage.line_numbers; this.line_numbers = settingsFromStorage.line_numbers;
this.time_position = settingsFromStorage.time_position; this.time_position = settingsFromStorage.time_position;
this.tips = settingsFromStorage.tips; this.tips = settingsFromStorage.tips;
this.completions = settingsFromStorage.completions;
this.send_clock = settingsFromStorage.send_clock; this.send_clock = settingsFromStorage.send_clock;
this.midi_channels_scripts = settingsFromStorage.midi_channels_scripts; this.midi_channels_scripts = settingsFromStorage.midi_channels_scripts;
this.midi_clock_input = settingsFromStorage.midi_clock_input; this.midi_clock_input = settingsFromStorage.midi_clock_input;
@ -193,6 +198,7 @@ export class AppSettings {
line_numbers: this.line_numbers, line_numbers: this.line_numbers,
time_position: this.time_position, time_position: this.time_position,
tips: this.tips, tips: this.tips,
completions: this.completions,
send_clock: this.send_clock, send_clock: this.send_clock,
midi_channels_scripts: this.midi_channels_scripts, midi_channels_scripts: this.midi_channels_scripts,
midi_clock_input: this.midi_clock_input, midi_clock_input: this.midi_clock_input,
@ -220,6 +226,7 @@ export class AppSettings {
this.line_numbers = settings.line_numbers; this.line_numbers = settings.line_numbers;
this.time_position = settings.time_position; this.time_position = settings.time_position;
this.tips = settings.tips; this.tips = settings.tips;
this.completions = settings.completions;
this.send_clock = settings.send_clock; this.send_clock = settings.send_clock;
this.midi_channels_scripts = settings.midi_channels_scripts; this.midi_channels_scripts = settings.midi_channels_scripts;
this.midi_clock_input = settings.midi_clock_input; this.midi_clock_input = settings.midi_clock_input;

View File

@ -21,6 +21,7 @@ import { loadSamples } from "./API";
import { tryEvaluate } from "./Evaluator"; import { tryEvaluate } from "./Evaluator";
import { inlineHoveringTips } from "./documentation/inlineHelp"; import { inlineHoveringTips } from "./documentation/inlineHelp";
import { lineNumbers } from "@codemirror/view"; import { lineNumbers } from "@codemirror/view";
import { jsCompletions } from "./EditorSetup";
export const installInterfaceLogic = (app: Editor) => { export const installInterfaceLogic = (app: Editor) => {
(app.interface.line_numbers_checkbox as HTMLInputElement).checked = (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.interface.time_position_checkbox as HTMLInputElement).checked =
app.settings.time_position; app.settings.time_position;
(app.interface.tips_checkbox as HTMLInputElement).checked = app.settings.tips; (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.interface.midi_clock_checkbox as HTMLInputElement).checked =
app.settings.send_clock; app.settings.send_clock;
(app.interface.midi_channels_scripts as HTMLInputElement).checked = (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", () => { app.interface.midi_clock_checkbox.addEventListener("change", () => {
let checked = (app.interface.midi_clock_checkbox as HTMLInputElement) let checked = (app.interface.midi_clock_checkbox as HTMLInputElement)
.checked .checked

71
src/Utils/Generic.ts Normal file
View File

@ -0,0 +1,71 @@
/*
* Transforms object with arrays into array of objects
*
* @param {Record<string, any>} input - Object with arrays
* @param {string[]} ignoredKeys - Keys to ignore
* @returns {Record<string, any>[]} Array of objects
*
*/
export function objectWithArraysToArrayOfObjects(input: Record<string, any>, ignoredKeys: string[]): Record<string, any>[] {
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<string, any>[] = [];
for (let i = 0; i < maxLength; i++) {
const event: Record<string, any> = {};
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<string, any>[]} array - Array of objects
* @param {Record<string, any>} mergeObject - Object that is merged to each object in the array
* @returns {object} Merged object with arrays
*
*/
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(array: T[], mergeObject: Record<string, any> = {}): Record<string, any> {
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<keyof T, any[]>);
}
/*
* Filter certain keys from object
*
* @param {Record<string, any>} obj - Object to filter
* @param {string[]} filter - Keys to filter
* @returns {object} Filtered object
*
*/
export function filterObject(obj: Record<string, any>, filter: string[]): Record<string, any> {
return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key)));
}

View File

@ -2,9 +2,7 @@ import { type Editor } from "../main";
import { import {
freqToMidi, freqToMidi,
resolvePitchBend, resolvePitchBend,
getScale, safeScale
isScale,
parseScala,
} from "zifferjs"; } from "zifferjs";
export abstract class Event { export abstract class Event {
@ -204,8 +202,20 @@ export abstract class Event {
return this.modify(func); return this.modify(func);
}; };
length = (value: number): Event => { noteLength = (value: number | number[], ...kwargs: number[]): Event => {
this.values["length"] = value; /**
* 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; return this;
}; };
} }
@ -215,37 +225,101 @@ export abstract class AudibleEvent extends Event {
super(app); super(app);
} }
octave = (value: number): this => { pitch = (value: number | number[], ...kwargs: number[]): this => {
this.values["octave"] = value; /*
this.update(); * This function is used to set the pitch of the Event.
return this; * @param value - The pitch value
}; * @returns The Event
*/
key = (value: string): this => { if(kwargs.length > 0) {
this.values["key"] = value; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
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[];
} }
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; 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; this.values["freq"] = value;
const midiNote = freqToMidi(value); if(Array.isArray(value)) {
if (midiNote % 1 !== 0) { this.values["note"] = [];
this.values["note"] = Math.floor(midiNote); this.values["bend"] = [];
this.values["bend"] = resolvePitchBend(midiNote)[1]; 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 { } 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; return this;
}; };

View File

@ -1,7 +1,8 @@
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main"; import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection"; 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 = { export type MidiParams = {
note: number; note: number;
@ -9,40 +10,49 @@ export type MidiParams = {
channel?: number; channel?: number;
port?: number; port?: number;
sustain?: number; sustain?: number;
velocity?: number;
} }
export class MidiEvent extends AudibleEvent { export class MidiEvent extends AudibleEvent {
midiConnection: MidiConnection; midiConnection: MidiConnection;
constructor(input: number | object, public app: Editor) { constructor(input: MidiParams, public app: Editor) {
super(app); super(app);
if (typeof input === "number") this.values["note"] = input; this.values = input;
else this.values = input;
this.midiConnection = app.api.MidiConnection; this.midiConnection = app.api.MidiConnection;
} }
chord = (value: MidiParams[]): this => { public chord = (value: string) => {
this.values["chord"] = value; this.values.note = parseChord(value);
return this; return this;
}; };
note = (value: number): this => { note = (value: number | number[]): this => {
this.values["note"] = value; this.values["note"] = value;
return this; return this;
}; };
sustain = (value: number): this => { sustain = (value: number | number[]): this => {
this.values["sustain"] = value; this.values["sustain"] = value;
return this; 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; this.values["channel"] = value;
return this; return this;
}; };
port = (value: number | string): this => { port = (value: number | string | number[] | string[]): this => {
this.values["port"] = this.midiConnection.getMidiOutputIndex(value); 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; return this;
}; };
@ -75,36 +85,45 @@ export class MidiEvent extends AudibleEvent {
}; };
update = (): void => { update = (): void => {
const [note, bend] = noteFromPc( // Get key, pitch, parsedScale and octave from this.values object
this.values.key || "C4", const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]);
this.values.pitch || 0,
this.values.parsedScale || "MAJOR", const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]);
this.values.octave || 0
); events.forEach((event) => {
this.values.note = note; const [note, bend] = noteFromPc(
this.values.freq = midiToFreq(note); event.key as number || "C4",
if (bend) this.values.bend = bend; 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 => { out = (): void => {
function play(event: MidiEvent, params?: MidiParams): void { function play(event: MidiEvent, params: MidiParams): void {
const paramChannel = params && params.channel ? params.channel : 0; const channel = params.channel ? params.channel : 0;
const channel = event.values.channel ? event.values.channel : paramChannel; const velocity = params.velocity ? params.velocity : 100;
const velocity = event.values.velocity ? event.values.velocity : 100; const note = params.note ? params.note : 60;
const paramNote = params && params.note ? params.note : 60;
const note = event.values.note ? event.values.note : paramNote;
const sustain = event.values.sustain const sustain = params.sustain
? event.values.sustain * ? params.sustain *
event.app.clock.pulse_duration * event.app.clock.pulse_duration *
event.app.api.ppqn() event.app.api.ppqn()
: 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 bend = params.bend ? params.bend : undefined;
const port = event.values.port const port = params.port
? event.midiConnection.getMidiOutputIndex(event.values.port) ? event.midiConnection.getMidiOutputIndex(params.port)
: event.midiConnection.getCurrentMidiPortIndex(); : event.midiConnection.getCurrentMidiPortIndex() || 0;
event.midiConnection.sendMidiNote( event.midiConnection.sendMidiNote(
note, note,
@ -116,13 +135,11 @@ export class MidiEvent extends AudibleEvent {
); );
} }
if(this.values.chord) { const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[];
this.values.chord.forEach((p: MidiParams) => {
play(this, p); events.forEach((p: MidiParams) => {
}); play(this,p);
} else { });
play(this);
}
}; };
} }

View File

@ -4,11 +4,11 @@ import { Event } from "./AbstractEvents";
export class RestEvent extends Event { export class RestEvent extends Event {
constructor(length: number, app: Editor) { constructor(length: number, app: Editor) {
super(app); super(app);
this.values["length"] = length; this.values["noteLength"] = length;
} }
_fallbackMethod = (): Event => { _fallbackMethod = (): Event => {
return RestEvent.createRestProxy(this.values["length"], this.app); return RestEvent.createRestProxy(this.values["noteLength"], this.app);
}; };
public static createRestProxy = ( public static createRestProxy = (

View File

@ -1,5 +1,10 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import {
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
import { import {
chord as parseChord, chord as parseChord,
midiToFreq, midiToFreq,
@ -13,328 +18,386 @@ import {
} from "superdough"; } from "superdough";
export type SoundParams = { export type SoundParams = {
dur: number; dur: number | number[];
s?: string; 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 { export class SoundEvent extends AudibleEvent {
nudge: number; 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); super(app);
this.nudge = app.dough_nudge / 100; 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<SoundParams>),
};
return validatedObj;
} else {
if (sound.includes(":")) { if (sound.includes(":")) {
this.values = { const vals = sound.split(":");
s: sound.split(":")[0], const s = vals[0];
n: sound.split(":")[1], const n = parseInt(vals[1]);
dur: app.clock.convertPulseToSecond(app.clock.ppqn), return {
s,
n,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true, analyze: true,
}; };
} else { } else {
this.values = { s: sound, dur: 0.5, analyze: true }; return { s: sound, dur: 0.5, analyze: true };
} }
} else {
this.values = sound;
} }
} };
private updateValue<T>(key: string, value: T): this { private updateValue<T>(
key: string,
value: T | T[] | SoundParams[] | null
): this {
if (value == null) return this;
this.values[key] = value; this.values[key] = value;
return this; return this;
} }
// ================================================================================ // ================================================================================
// ZZFX Sound Parameters // AbstactEvent overrides
// ================================================================================ // ================================================================================
public volume = (value: number) => this.updateValue("volume", value); modify = (func: Function): this => {
public vol = this.volume; const funcResult = func(this);
public zrand = (value: number) => this.updateValue("zrand", value); if (funcResult instanceof Object) return funcResult;
public curve = (value: number) => this.updateValue("curve", value); else {
public slide = (value: number) => this.updateValue("slide", value); func(this.values);
public sld = this.slide; this.update();
public deltaSlide = (value: number) => this.updateValue("deltaSlide", value); return this;
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)
} }
return this; };
}
public lpf = this.cutoff; update = (): void => {
public resonance = (value: number) => { const filteredValues = filterObject(this.values, [
if (value >= 0 && value <= 1) { "key",
this.updateValue( "pitch",
"resonance", "parsedScale",
50 * value "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
); );
} event.note = note;
return this; event.freq = midiToFreq(note);
} });
public lpq = this.resonance;
public lpadsr = ( const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams;
depth: number,
a: number, this.values.note = newArrays.note;
d: number, this.values.freq = newArrays.freq;
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;
}; };
public chord = (value: string) => {
// Highpass filter const chord = parseChord(value);
return this.updateValue("note", chord);
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;
}; };
// 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) => { public invert = (howMany: number = 0) => {
if (this.values.chord) { if (this.values.chord) {
let notes = this.values.chord.map( let notes = this.values.chord.map(
@ -352,10 +415,6 @@ export class SoundEvent extends AudibleEvent {
return this; 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) => { public note = (value: number | string | null) => {
if (typeof value === "string") { if (typeof value === "string") {
return this.updateValue("note", noteNameToMidi(value)); return this.updateValue("note", noteNameToMidi(value));
@ -365,109 +424,13 @@ export class SoundEvent extends AudibleEvent {
return this.updateValue("note", value); 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 => { out = (): void => {
console.log(this.app.clock.deviation) const events = objectWithArraysToArrayOfObjects(this.values, [
if (this.values.chord) { "parsedScale",
this.values.chord.forEach((obj: { [key: string]: number }) => { ]);
const copy = { ...this.values }; for (const event of events) {
copy.freq = obj.freq; superdough(event, this.nudge, event.dur);
superdough(copy, this.nudge - this.app.clock.deviation, this.values.dur);
});
} else {
superdough(this.values, this.nudge - this.app.clock.deviation, this.values.dur);
} }
}; };
} }

View File

@ -5,6 +5,7 @@ import { SkipEvent } from "./SkipEvent";
import { SoundEvent, SoundParams } from "./SoundEvent"; import { SoundEvent, SoundParams } from "./SoundEvent";
import { MidiEvent, MidiParams } from "./MidiEvent"; import { MidiEvent, MidiParams } from "./MidiEvent";
import { RestEvent } from "./RestEvent"; import { RestEvent } from "./RestEvent";
import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
export type InputOptions = { [key: string]: string | number }; export type InputOptions = { [key: string]: string | number };
@ -23,7 +24,12 @@ export class Player extends Event {
options: InputOptions = {}; options: InputOptions = {};
skipIndex = 0; skipIndex = 0;
constructor(input: string, options: InputOptions, public app: Editor, zid: string = "") { constructor(
input: string,
options: InputOptions,
public app: Editor,
zid: string = ""
) {
super(app); super(app);
this.input = input; this.input = input;
this.options = options; this.options = options;
@ -108,15 +114,17 @@ export class Player extends Event {
this.app.api.resetAllFromCache(); this.app.api.resetAllFromCache();
} }
const patternIsStarting = (this.notStarted() && const patternIsStarting =
this.notStarted() &&
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) && (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.origin()) >=
this.pulseToSecond(this.lastCallTime) + this.pulseToSecond(this.lastCallTime) +
this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) && this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) &&
this.origin() >= this.waitTime); this.origin() >= this.waitTime;
// If pattern is starting or it's time to play next event // If pattern is starting or it's time to play next event
const areWeThereYet = patternIsStarting || timeToPlayNext; const areWeThereYet = patternIsStarting || timeToPlayNext;
@ -139,37 +147,44 @@ export class Player extends Event {
}; };
sound(name?: string) { sound(name?: string) {
if (this.areWeThereYet()) { if (this.areWeThereYet()) {
const event = this.next() as Pitch | Chord | ZRest; 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) { if (event instanceof Pitch) {
const obj = event.getExisting( const obj = event.getExisting(
"freq", "freq",
"note",
"pitch", "pitch",
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale"
); ) as SoundParams;
if (event.sound) name = event.sound as string; 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; obj.dur = noteLengthInSeconds;
return new SoundEvent(obj, this.app).sound(name || "sine"); return new SoundEvent(obj, this.app).sound(name || "sine");
} else if (event instanceof Chord) { } else if (event instanceof Chord) {
const pitches = event.pitches.map((p) => { const pitches = event.pitches.map((p) => {
return p.getExisting( return p.getExisting(
"freq", "freq",
"note",
"pitch", "pitch",
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale"
); );
}); }) as SoundParams[];
const sound: SoundParams = { dur: noteLengthInSeconds }; const add = { dur: noteLengthInSeconds } as SoundParams;
if (name) sound.s = name; if (name) add.s = name;
return new SoundEvent(sound, this.app).chord(pitches); let sound = arrayOfObjectsToObjectWithArrays(
pitches,
add
) as SoundParams;
return new SoundEvent(sound, this.app);
} else if (event instanceof ZRest) { } else if (event instanceof ZRest) {
return RestEvent.createRestProxy(event.duration, this.app); return RestEvent.createRestProxy(event.duration, this.app);
} }
@ -188,17 +203,18 @@ export class Player extends Event {
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale", "parsedScale"
); ) as MidiParams;
if (event instanceof Pitch) { 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); const note = new MidiEvent(obj, this.app);
return value ? note.note(value) : note; return value ? note.note(value) : note;
} else if (event instanceof ZRest) { } else if (event instanceof ZRest) {
return RestEvent.createRestProxy(event.duration, this.app); return RestEvent.createRestProxy(event.duration, this.app);
} else if (event instanceof Chord) { } else if (event instanceof Chord) {
const pitches = event.midiChord() as MidiParams[]; 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 { } else {
return SkipEvent.createSkipProxy(); return SkipEvent.createSkipProxy();
@ -232,7 +248,7 @@ export class Player extends Event {
this.ziffers.invert(n); this.ziffers.invert(n);
} }
return this; return this;
} };
retrograde() { retrograde() {
if (this.atTheBeginning()) this.ziffers.retrograde(); if (this.atTheBeginning()) this.ziffers.retrograde();

View File

@ -1,5 +1,7 @@
import { hoverTooltip } from "@codemirror/view"; import { hoverTooltip } from "@codemirror/view";
import { type EditorView } from "@codemirror/view"; import { type EditorView } from "@codemirror/view";
import { CompletionContext } from "@codemirror/autocomplete"
interface InlineCompletion { interface InlineCompletion {
name: string; name: string;
@ -23,7 +25,7 @@ const completionDatabase: CompletionDatabase = {
name: "delayr", name: "delayr",
category: "time", category: "time",
description: "Delay a function <i>n</i> times by <i>t</i> ms", description: "Delay a function <i>n</i> times by <i>t</i> ms",
example: "delayr(50, 3, () => beat(1) :: log('delayed'))", example: "delayr(50,3,()=> beat(1)::log('hey!'))",
}, },
toss: { toss: {
name: "toss", name: "toss",
@ -35,7 +37,7 @@ const completionDatabase: CompletionDatabase = {
name: "lpadsr", name: "lpadsr",
category: "synthesis", category: "synthesis",
description: "Lowpass filter ADSR envelope", 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: { lpenv: {
name: "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 = `
<h1 class="text-orange-300 text-base pb-1">${completionDatabase[key].name} [<em class="text-white">${completionDatabase[key].category}</em>]</h1>
<p class="text-base pl-4">${completionDatabase[key].description}</p>
<div class="overflow-hidden overflow-scroll rounded px-2 ml-4 mt-2 bg-neutral-800"><code class="text-sm">${completionDatabase[key].example}</code></div>
`
div.classList.add("px-4", "py-2", "rounded-lg", "w-92");
return div
}
}))
}
}
}

View File

@ -42,6 +42,7 @@ export class Editor {
withLineNumbers!: Compartment; withLineNumbers!: Compartment;
vimModeCompartment!: Compartment; vimModeCompartment!: Compartment;
hoveringCompartment!: Compartment; hoveringCompartment!: Compartment;
completionsCompartment!: Compartment;
chosenLanguage!: Compartment; chosenLanguage!: Compartment;
dynamicPlugins!: Compartment; dynamicPlugins!: Compartment;
currentDocumentationPane: string = "introduction"; currentDocumentationPane: string = "introduction";
@ -64,7 +65,7 @@ export class Editor {
color: "#fdba74", color: "#fdba74",
thickness: 4, thickness: 4,
refresh: 1, refresh: 1,
fftSize: 256, fftSize: 1024,
orientation: "horizontal", orientation: "horizontal",
is3D: false, is3D: false,
size: 1, size: 1,