From 6c93845a604dc785bc21d01eeb9aad50d0200d3b Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 21 Aug 2023 18:46:07 +0300 Subject: [PATCH] Event superclass for Note and Sound --- package.json | 2 +- src/API.ts | 104 +++++++++++++++++++++++++++------------ src/Event.ts | 53 ++++++++++++++++++++ src/IO/MidiConnection.ts | 50 +++++++++++++++++-- src/Note.ts | 78 +++++++++++++++++++++++++++++ src/Pattern.ts | 101 ------------------------------------- src/Sound.ts | 29 +++++++++-- yarn.lock | 8 +-- 8 files changed, 278 insertions(+), 147 deletions(-) create mode 100644 src/Event.ts create mode 100644 src/Note.ts delete mode 100644 src/Pattern.ts diff --git a/package.json b/package.json index 3278cc9..adca71e 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/API.ts b/src/API.ts index 645a1d1..c3784f5 100644 --- a/src/API.ts +++ b/src/API.ts @@ -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(); 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): 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 = (...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; }; // ============================================================= diff --git a/src/Event.ts b/src/Event.ts new file mode 100644 index 0000000..f1732b6 --- /dev/null +++ b/src/Event.ts @@ -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); + } + +} \ No newline at end of file diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 0927332..b238a66 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -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]; diff --git a/src/Note.ts b/src/Note.ts new file mode 100644 index 0000000..3fbd170 --- /dev/null +++ b/src/Note.ts @@ -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); + } + +} \ No newline at end of file diff --git a/src/Pattern.ts b/src/Pattern.ts deleted file mode 100644 index ecede2f..0000000 --- a/src/Pattern.ts +++ /dev/null @@ -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(); - -// 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; - -} \ No newline at end of file diff --git a/src/Sound.ts b/src/Sound.ts index cc2ad30..8e7dd85 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -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); } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1c15dce..2c5060a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"