Event superclass for Note and Sound

This commit is contained in:
2023-08-21 18:46:07 +03:00
parent 0664625923
commit 6c93845a60
8 changed files with 278 additions and 147 deletions

View File

@ -37,7 +37,7 @@
"tailwindcss": "^3.3.3",
"tone": "^14.8.49",
"vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.8",
"zifferjs": "^0.0.9",
"zzfx": "^1.2.0"
}
}

View File

@ -1,4 +1,4 @@
import { Pitch, Chord, Rest, Event, cachedPattern } from "zifferjs";
import { Pitch, Chord, Rest, Event, cachedPattern, seededRandom } from "zifferjs";
import { MidiConnection } from "./IO/MidiConnection";
import { tryEvaluate } from "./Evaluator";
import { DrunkWalk } from "./Utils/Drunk";
@ -6,6 +6,7 @@ import { LRUCache } from "lru-cache";
import { scale } from "./Scales";
import { Editor } from "./main";
import { Sound } from "./Sound";
import { Note } from "./Note";
import {
samples,
initAudioOnFirstClick,
@ -68,6 +69,9 @@ export class UserAPI {
private variables: { [key: string]: any } = {};
private iterators: { [key: string]: any } = {};
private _drunk: DrunkWalk = new DrunkWalk(-100, 100, false);
public randomGen = Math.random;
public currentSeed: string|undefined = undefined;
public localSeeds = new Map<string, Function>();
MidiConnection: MidiConnection = new MidiConnection();
load: samples;
@ -105,6 +109,21 @@ export class UserAPI {
return this.app._mouseY;
};
public noteX = (): number => {
/**
* @returns The current x position scaled to 0-127 using screen width
*/
return Math.floor((this.app._mouseX / document.body.clientWidth) * 127);
};
public noteY = (): number => {
/**
* @returns The current y position scaled to 0-127 using screen height
*/
return Math.floor((this.app._mouseY / document.body.clientHeight) * 127);
};
// =============================================================
// Utility functions
// =============================================================
@ -179,9 +198,8 @@ export class UserAPI {
};
public note = (
note: number,
options: { [key: string]: number } = {}
): void => {
value: number = 60
): Note => {
/**
* Sends a MIDI note to the current MIDI output.
*
@ -189,10 +207,7 @@ export class UserAPI {
* @param options - an object containing options for that note
* { channel: 0, velocity: 100, duration: 0.5 }
*/
const channel = options.channel ? options.channel : 0;
const velocity = options.velocity ? options.velocity : 100;
const duration = options.duration ? options.duration : 0.5;
this.MidiConnection.sendMidiNote(note, channel, velocity, duration);
return new Note(value, this.app);
};
public sysex = (data: Array<number>): void => {
@ -261,27 +276,26 @@ export class UserAPI {
public zn = (
input: string,
options: { [key: string]: string | number } = {}
): Event => {
): Event|object => {
const pattern = cachedPattern(input, options);
//@ts-ignore
if (pattern.hasStarted()) {
const event = pattern.peek();
// Check if event is modified
const node = event!.modifiedEvent ? event!.modifiedEvent : event;
// Check if event is modified
const node = event.modifiedEvent ? event.modifiedEvent : event;
const channel = (options.channel ? options.channel : 0) as number;
const velocity = (options.velocity ? options.velocity : 100) as number;
const sustain = (options.sustain ? options.sustain : 0.5) as number;
if (node instanceof Pitch) {
if (node.bend) this.MidiConnection.sendPitchBend(node.bend, channel);
this.MidiConnection.sendMidiNote(
node.note!,
channel,
velocity,
sustain
);
if (node.bend) this.MidiConnection.sendPitchBend(8192, channel);
this.MidiConnection.sendMidiNote(
node.note!,
channel,
velocity,
sustain
);
if (node.bend) this.MidiConnection.sendPitchBend(8192, channel);
} else if (node instanceof Chord) {
node.pitches.forEach((pitch: Pitch) => {
if (pitch.bend)
@ -299,7 +313,7 @@ export class UserAPI {
}
// Remove old modified event
if (event!.modifiedEvent) event!.modifiedEvent = undefined;
if (event.modifiedEvent) event.modifiedEvent = undefined;
}
//@ts-ignore
return pattern.next();
@ -577,7 +591,7 @@ export class UserAPI {
*
* @param array - The array of values to pick from
*/
return array[Math.floor(Math.random() * array.length)];
return array[Math.floor(this.randomGen() * array.length)];
};
seqbeat = <T>(...array: T[]): T => {
@ -629,7 +643,7 @@ export class UserAPI {
* @param max - The maximum value of the random number
* @returns A random integer between min and max
*/
return Math.floor(Math.random() * (max - min + 1)) + min;
return Math.floor(this.randomGen() * (max - min + 1)) + min;
};
rand = (min: number, max: number): number => {
@ -640,11 +654,37 @@ export class UserAPI {
* @param max - The maximum value of the random number
* @returns A random float between min and max
*/
return Math.random() * (max - min) + min;
return this.randomGen() * (max - min) + min;
};
irand = this.randI
rI = this.randI;
r = this.rand;
seed = (seed: string | number): void => {
/**
* Seed the random numbers globally in UserAPI.
* @param seed - The seed to use
*/
if(typeof seed === "number") seed = seed.toString();
if(this.currentSeed!==seed) {
this.currentSeed = seed;
this.randomGen = seededRandom(seed);
}
}
localSeededRandom = (seed: string | number): Function => {
if(typeof seed === "number") seed = seed.toString();
if(this.localSeeds.has(seed)) return this.localSeeds.get(seed) as Function;
const newSeededRandom = seededRandom(seed)
this.localSeeds.set(seed,newSeededRandom);
return newSeededRandom;
}
clearLocalSeed = (seed: string | number | undefined = undefined): void => {
if(seed) this.localSeeds.delete(seed.toString());
this.localSeeds.clear();
}
// =============================================================
// Quantification functions
// =============================================================
@ -748,7 +788,7 @@ export class UserAPI {
*
* @returns True 10% of the time
*/
return Math.random() > 0.9;
return this.randomGen() > 0.9;
};
public sometimes = (): boolean => {
@ -757,7 +797,7 @@ export class UserAPI {
*
* @returns True 50% of the time
*/
return Math.random() > 0.5;
return this.randomGen() > 0.5;
};
public rarely = (): boolean => {
@ -766,7 +806,7 @@ export class UserAPI {
*
* @returns True 25% of the time
*/
return Math.random() > 0.75;
return this.randomGen() > 0.75;
};
public often = (): boolean => {
@ -775,7 +815,7 @@ export class UserAPI {
*
* @returns True 75% of the time
*/
return Math.random() > 0.25;
return this.randomGen() > 0.25;
};
public almostAlways = (): boolean => {
@ -784,7 +824,7 @@ export class UserAPI {
*
* @returns True 90% of the time
*/
return Math.random() > 0.1;
return this.randomGen() > 0.1;
};
public dice = (sides: number): number => {
@ -794,7 +834,7 @@ export class UserAPI {
* @param sides - The number of sides on the dice
* @returns The value of a dice roll with n sides
*/
return Math.floor(Math.random() * sides) + 1;
return Math.floor(this.randomGen() * sides) + 1;
};
// =============================================================
@ -920,7 +960,7 @@ export class UserAPI {
* @param p - The probability of returning true
* @returns True p% of the time
*/
return Math.random() * 100 < p;
return this.randomGen() * 100 < p;
};
toss = (): boolean => {
@ -934,7 +974,7 @@ export class UserAPI {
* @see almostAlways
* @see almostNever
*/
return Math.random() > 0.5;
return this.randomGen() > 0.5;
};
min = (...values: number[]): number => {
@ -1212,7 +1252,7 @@ export class UserAPI {
* @see sine
* @see noise
*/
return Math.random() * 2 - 1;
return this.randomGen() * 2 - 1;
};
// =============================================================

53
src/Event.ts Normal file
View File

@ -0,0 +1,53 @@
import { type Editor } from './main';
export class Event {
seedValue: string|undefined = undefined;
randomGen: Function = Math.random;
app: Editor;
constructor(app: Editor) {
this.app = app;
if(this.app.api.currentSeed) {
this.randomGen = this.app.api.randomGen;
}
}
sometimesBy = (probability: number, func: Function): Event => {
if(this.randomGen() < probability) {
return this.modify(func);
}
return this;
}
sometimes = (func: Function): Event => {
return this.sometimesBy(0.5, func);
}
rarely = (func: Function): Event => {
return this.sometimesBy(0.1, func);
}
often = (func: Function): Event => {
return this.sometimesBy(0.9, func);
}
modify = (func: Function): Event => {
return func(this);
}
seed = (value: string|number): Event => {
this.seedValue = value.toString();
this.randomGen = this.app.api.localSeededRandom(this.seedValue);
return this;
}
clear = (): Event => {
this.app.api.clearLocalSeed(this.seedValue);
return this;
}
apply = (func: Function): Event => {
return this.modify(func);
}
}

View File

@ -30,6 +30,7 @@ export class MidiConnection{
this.midiOutputs = Array.from(this.midiAccess.outputs.values());
if (this.midiOutputs.length === 0) {
console.warn('No MIDI outputs available.');
this.currentOutputIndex = -1;
}
} catch (error) {
console.error('Failed to initialize MIDI:', error);
@ -50,6 +51,20 @@ export class MidiConnection{
}
}
public getCurrentMidiPortIndex(): number {
/**
* Returns the index of the currently selected MIDI output.
*
* @returns Index of the currently selected MIDI output or -1 if no MIDI output is selected or available.
*/
if(this.midiOutputs.length > 0 && this.currentOutputIndex >= 0 && this.currentOutputIndex < this.midiOutputs.length) {
return this.currentOutputIndex;
} else {
console.error('No MIDI output selected or available.');
return -1;
}
}
public sendMidiClock(): void {
/**
* Sends a single MIDI clock message to the currently selected MIDI output.
@ -69,15 +84,40 @@ export class MidiConnection{
* @param outputName Name of the MIDI output to switch to
* @returns True if the MIDI output was found and switched to, false otherwise
*/
const index = this.midiOutputs.findIndex((output) => output.name === outputName);
const index = this.getMidiOutputIndex(outputName);
if (index !== -1) {
this.currentOutputIndex = index;
return true;
} else {
console.error(`MIDI output "${outputName}" not found.`);
return false;
}
}
public getMidiOutputIndex(output: string|number): number {
/**
* Returns the index of the MIDI output with the specified name.
*
* @param outputName Name of the MIDI output
* @returns Index of the new MIDI output or current output if new is not valid
*
*/
if(typeof output === 'number') {
if (output < 0 || output >= this.midiOutputs.length) {
console.error(`Invalid MIDI output index. Index must be in the range 0-${this.midiOutputs.length - 1}.`);
return this.currentOutputIndex;
} else {
return output;
}
} else {
const index = this.midiOutputs.findIndex((o) => o.name === output);
if (index !== -1) {
return index;
} else {
console.error(`MIDI output "${output}" not found.`);
return this.currentOutputIndex;
}
}
}
public listMidiOutputs(): void {
/**
@ -89,7 +129,7 @@ export class MidiConnection{
});
}
public sendMidiNote(noteNumber: number, channel: number, velocity: number, duration: number): void {
public sendMidiNote(noteNumber: number, channel: number, velocity: number, duration: number, port: number|string = this.currentOutputIndex): void {
/**
* Sending a MIDI Note on/off message with the same note number and channel. Automatically manages
* the note off message after the specified duration.
@ -100,7 +140,9 @@ export class MidiConnection{
* @param duration Duration in milliseconds
*
*/
const output = this.midiOutputs[this.currentOutputIndex];
if(typeof port === 'string') port = this.getMidiOutputIndex(port);
const output = this.midiOutputs[port];
noteNumber = Math.min(Math.max(noteNumber, 0), 127);
if (output) {
const noteOnMessage = [0x90 + channel, noteNumber, velocity];

78
src/Note.ts Normal file
View File

@ -0,0 +1,78 @@
import { Event } from './Event';
import { type Editor } from './main';
import { MidiConnection } from "./IO/MidiConnection";
export class Note extends Event {
values: { [key: string]: any };
midiConnection: MidiConnection;
constructor(input: number|object, public app: Editor) {
super(app);
if(typeof input === 'number') input = { 'note': input };
this.values = input;
this.midiConnection = app.api.MidiConnection
}
note = (value: number): this => {
this.values['note'] = value;
return this;
}
channel = (value: number): this => {
this.values['channel'] = value;
return this;
}
port = (value: number|string): this => {
this.values['port'] = this.midiConnection.getMidiOutputIndex(value);
return this;
}
add = (value: number): this => {
this.values.note += value;
return this;
}
modify = (func: Function): this => {
const funcResult = func(this);
if(funcResult instanceof Object) return funcResult;
else {
func(this.values);
return this;
}
}
// TODO: Add bend
freq = (value: number): this => {
this.values['freq'] = value;
return this;
}
bend = (value: number): this => {
this.values['bend'] = value;
return this;
}
random = (min: number = 0, max: number = 127): this => {
min = Math.min(Math.max(min, 0), 127);
max = Math.min(Math.max(max, 0), 127);
this.values['note'] = Math.floor(this.randomGen() * (max - min + 1)) + min;
return this;
}
out = (): void => {
const note = this.values.note ? this.values.note : 60;
const channel = this.values.channel ? this.values.channel : 0;
const velocity = this.values.velocity ? this.values.velocity : 100;
const duration = this.values.duration ? this.values.duration : 0.5;
const bend = this.values.bend ? this.values.bend : undefined;
const port = this.values.port ?
this.midiConnection.getMidiOutputIndex(this.values.port) :
this.midiConnection.getCurrentMidiPortIndex();
if (bend) this.midiConnection.sendPitchBend(bend, channel);
this.midiConnection.sendMidiNote(note, channel, velocity, duration, port);
if (bend) this.midiConnection.sendPitchBend(8192, channel);
}
}

View File

@ -1,101 +0,0 @@
export class Pattern {
events: Event[];
_current : Event | undefined = undefined;
constructor(values: number[]) {
this.events = values.map((value) => new Event(value));
this.buildLinks();
}
// Create links cyclic links between events
buildLinks(): void {
this.events.forEach((event, index) => {
event._next = index < this.events.length - 1 ? this.events[index + 1] : this.events[0];
});
}
// Get the current event for this pattern
current(): Event {
if(this._current) this._current = this._current.next();
else this._current = this.events[0];
return this._current;
}
}
export class Event {
/**
* Simple Event class with simple numerical value and link to next event
*/
_next!: Event;
_value: number;
// Used to store a modified version of the event
modifiedEvent: Event | undefined = undefined;
constructor(value: number) {
this._value = value;
}
get value(): number {
if(this.modifiedEvent) return this.modifiedEvent._value;
return this._value;
}
add(value: number): Event {
if(!this.modifiedEvent) this.modifiedEvent = this.clone();
this.modifiedEvent._value += value;
return this;
}
next() {
if(this.modifiedEvent) {
const next = this.modifiedEvent._next;
// Set modifiedEvent to undefined, cos we dont want to apply methods to earlier modified events
this.modifiedEvent = undefined;
return next;
}
return this._next;
}
clone(): Event {
const event = new Event(this._value);
event._next = this._next;
return event;
}
}
// Simple cache for patterns
let cache = new Map<string, Pattern>();
// Create a cache key from the values of a pattern somehow
const createCacheKey = (values: number[]) => values.join('-');
// Get a cached pattern or create a new one
const getCachedPattern = (values: number[]) => {
const key = createCacheKey(values);
const cachedPattern = cache.get(key);
if(cachedPattern) return cachedPattern;
const newPattern = new Pattern(values);
cache.set(key, newPattern);
return newPattern;
}
// Cached event function that includes the main logic
const cachedEvent = (values: number[]): Event => {
const pattern = getCachedPattern(values);
if(pattern._current) { console.log("Play: ", pattern._current.value) }
else { console.log("Current is undefined so just starting!") }
return pattern.current();
}
// Test it out
let i = 0;
while(true) {
cachedEvent([1, 2, 3]).add(1).add(-2);
if(i++>10) break;
}

View File

@ -1,18 +1,28 @@
import { type Editor } from './main';
import { Event } from './Event';
import {
superdough,
// @ts-ignore
} from "superdough";
export class Sound {
export class Sound extends Event {
values: { [key: string]: any }
constructor(sound: string, public app: Editor) {
this.values = { 's': sound }
constructor(sound: string|object, public app: Editor) {
super(app);
if (typeof sound === 'string') this.values = { 's': sound };
else this.values = sound;
}
sound = (value: string): this => {
this.values['s'] = value
return this;
}
snd = this.sound;
unit = (value: number): this => {
this.values['unit'] = value
return this;
@ -163,7 +173,16 @@ export class Sound {
return this;
}
out = (): object => {
return superdough(this.values, this.app.clock.pulse_duration);
modify = (func: Function): this => {
const funcResult = func(this);
if(funcResult instanceof Object) return funcResult;
else {
func(this.values);
return this;
}
}
out = (): void => {
superdough(this.values, this.app.clock.pulse_duration);
}
}

View File

@ -1441,10 +1441,10 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
zifferjs@^0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.8.tgz#4e165679f37d81f2a02399f617ddb3c7fc1738ba"
integrity sha512-yxRo+BVZiHDoZksLHtAgkE/e5qeRboj3jcx1DDmdr9zrQUGBea+WQzfeo0IOrFnzbN/D7A7g9Vy4acJ+1R6z6g==
zifferjs@^0.0.9:
version "0.0.9"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.9.tgz#47037cee6dd161838dd236bdbc3eda9b099e2281"
integrity sha512-XS/JAc9nkmoiRaT/YFuX7r1ROvApQnY5BxOKyenAeDATvKZ80sIoXUw48U27KTsuJIsiPInNm5RieJGCJkoVmQ==
dependencies:
lru-cache "^10.0.0"