diff --git a/package.json b/package.json index 6b14fe9..346aeef 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "astring": "^1.8.6", "autoprefixer": "^10.4.14", "codemirror": "^6.0.1", - "lru-cache": "^10.0.1", "marked": "^7.0.3", "postcss": "^8.4.27", "showdown": "^2.1.0", @@ -37,7 +36,7 @@ "tailwindcss": "^3.3.3", "tone": "^14.8.49", "vite-plugin-markdown": "^2.1.0", - "zifferjs": "^0.0.10", + "zifferjs": "^0.0.11", "zzfx": "^1.2.0" } } diff --git a/src/API.ts b/src/API.ts index cb06d71..b7465ec 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1,4 +1,4 @@ -import { Pitch, Chord, Rest, Event, cachedPattern, seededRandom } from "zifferjs"; +import { seededRandom } from "zifferjs"; import { MidiConnection } from "./IO/MidiConnection"; import { tryEvaluate } from "./Evaluator"; import { DrunkWalk } from "./Utils/Drunk"; @@ -6,6 +6,8 @@ import { scale } from "./Scales"; import { Editor } from "./main"; import { Sound } from "./Sound"; import { Note } from "./Note"; +import { LRUCache } from "lru-cache"; +import { Player } from "./ZPlayer"; import { samples, initAudioOnFirstClick, @@ -45,6 +47,10 @@ async function loadSamples() { loadSamples(); +export const generateCacheKey = (...args: any[]): string => { + return args.map(arg => JSON.stringify(arg)).join(','); +}; + export class UserAPI { /** * The UserAPI class is the interface between the user's code and the backend. It provides @@ -59,6 +65,7 @@ export class UserAPI { public randomGen = Math.random; public currentSeed: string|undefined = undefined; public localSeeds = new Map(); + public patternCache = new LRUCache({max: 1000, ttl: 1000 * 60 * 5}); MidiConnection: MidiConnection = new MidiConnection(); load: samples; @@ -74,7 +81,7 @@ export class UserAPI { this.app.error_line.classList.remove('hidden'); setInterval(() => this.app.error_line.classList.add('hidden'), 2000) } - } + }; // ============================================================= // Time functions @@ -286,51 +293,21 @@ export class UserAPI { // Ziffers related functions // ============================================================= - public zn = ( - input: string, - options: { [key: string]: string | number } = {} - ): 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; - 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); - } else if (node instanceof Chord) { - node.pitches.forEach((pitch: Pitch) => { - if (pitch.bend) - this.MidiConnection.sendPitchBend(pitch.bend, channel); - this.MidiConnection.sendMidiNote( - pitch.note!, - channel, - velocity, - sustain - ); - if (pitch.bend) this.MidiConnection.sendPitchBend(8192, channel); - }); - } else if (node instanceof Rest) { - // do nothing for now ... + public z = (input: string, options: { [key: string]: string | number } = {}) => { + const key = generateCacheKey(input, options); + let player; + if(this.app.api.patternCache.has(key)) { + player = this.app.api.patternCache.get(key) as Player; + } else { + player = new Player(input, options, this.app); + this.app.api.patternCache.set(key, player); } - - // Remove old modified event - if (event.modifiedEvent) event.modifiedEvent = undefined; + if(player && player.ziffers.index === -1 || player.played) { + player.callTime = this.epulse(); + player.played = false; + } + return player; } - //@ts-ignore - return pattern.next(); - }; // ============================================================= // Counter related functions diff --git a/src/Event.ts b/src/Event.ts index a5da575..ba98230 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -4,6 +4,7 @@ export class Event { seedValue: string|undefined = undefined; randomGen: Function = Math.random; app: Editor; + values: { [key: string]: any } = {}; constructor(app: Editor) { this.app = app; @@ -66,4 +67,9 @@ export class Event { return this.modify(func); } + duration = (value: number): Event => { + this.values['duration'] = value; + return this; + } + } \ No newline at end of file diff --git a/src/Note.ts b/src/Note.ts index bfe380a..7045be8 100644 --- a/src/Note.ts +++ b/src/Note.ts @@ -1,16 +1,15 @@ import { Event } from './Event'; import { type Editor } from './main'; import { MidiConnection } from "./IO/MidiConnection"; -import { freqToMidi, resolvePitchBend } from 'zifferjs'; +import { freqToMidi, midiToFreq, resolvePitchBend, noteFromPc, getScale, isScale, parseScala } from 'zifferjs'; 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; + if(typeof input === 'number') this.values['note'] = input; + else this.values = input; this.midiConnection = app.api.MidiConnection } @@ -24,6 +23,11 @@ export class Note extends Event { return this; } + sustain = (value: number): this => { + this.values['sustain'] = value; + return this; + } + channel = (value: number): this => { this.values['channel'] = value; return this; @@ -75,13 +79,49 @@ export class Note extends Event { return this; } + update = (): void => { + console.log(this.values.type); + if(this.values.type === 'Pitch') { + const [note,bend] = noteFromPc(this.values.key!, this.values.pitch!, this.values.parsedScale!, this.values.octave!); + this.values.note = note; + this.values.freq = midiToFreq(note); + if(bend) { + this.values.bend = bend; + } + } + } + + octave = (value: number): this => { + this.values['octave'] = value; + this.update(); + return this; + } + + key = (value: string): this => { + this.values['key'] = value; + this.update(); + return this; + } + + scale = (value: string): this => { + if(!isScale(value)) { + this.values.parsedScale = parseScala(value) as number[]; + } else { + this.values.scaleName = value; + this.values.parsedScale = getScale(value) as number[]; + } + this.update(); + return this; + } + out = (): void => { + console.log("NOTE OUT", this.values); 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 * this.app.clock.pulse_duration * this.app.api.ppqn() : + const sustain = this.values.sustain ? + this.values.sustain * this.app.clock.pulse_duration * this.app.api.ppqn() : this.app.clock.pulse_duration * this.app.api.ppqn(); const bend = this.values.bend ? this.values.bend : undefined; @@ -90,7 +130,7 @@ export class Note extends Event { this.midiConnection.getMidiOutputIndex(this.values.port) : this.midiConnection.getCurrentMidiPortIndex(); - this.midiConnection.sendMidiNote(note, channel, velocity, duration, port, bend); + this.midiConnection.sendMidiNote(note, channel, velocity, sustain, port, bend); } } \ No newline at end of file diff --git a/src/Rest.ts b/src/Rest.ts new file mode 100644 index 0000000..8898af6 --- /dev/null +++ b/src/Rest.ts @@ -0,0 +1,33 @@ +import { type Editor } from './main'; +import { Event } from "./Event"; + +export class Rest extends Event { + constructor(duration: number, app: Editor) { + super(app); + this.values["duration"] = duration; + } + + _fallbackMethod = (): Event => { + return this; + } + + public static createRestProxy = (duration: number, app: Editor) => { + const instance = new Rest(duration, app); + return new Proxy(instance, { + // @ts-ignore + get(target, propKey, receiver) { + // @ts-ignore + if (typeof target[propKey] === 'undefined') { + return target._fallbackMethod; + } + // @ts-ignore + return target[propKey]; + }, + }); + } + + out = (): void => { + // TODO? + } + +} \ No newline at end of file diff --git a/src/Sound.ts b/src/Sound.ts index 9fbd64f..e548842 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -7,7 +7,6 @@ import { } from "superdough"; export class Sound extends Event { - values: { [key: string]: any }; constructor(sound: string|object, public app: Editor) { super(app); diff --git a/src/ZPlayer.ts b/src/ZPlayer.ts new file mode 100644 index 0000000..0e399da --- /dev/null +++ b/src/ZPlayer.ts @@ -0,0 +1,66 @@ +import { Chord, Pitch, Rest as ZRest, Ziffers } from "zifferjs"; +import { Editor } from "./main"; +import { Event } from "./Event"; +import { Sound } from "./Sound"; +import { Note } from "./Note"; +import { Rest } from "./Rest"; + +export class Player extends Event { + input: string; + ziffers: Ziffers; + callTime: number = 0; + played: boolean = false; + current!: Pitch|Chord|ZRest; + + constructor(input: string, options: object, public app: Editor) { + super(app); + this.input = input; + this.ziffers = new Ziffers(input, options); + } + + next = (): Pitch|Chord|ZRest => { + this.current = this.ziffers.next() as Pitch|Chord|ZRest; + this.played = true; + return this.current; + } + + areWeThereYet = (): boolean => { + return (this.ziffers.notStarted() || this.app.api.epulse() > this.callTime+(this.current.duration*this.app.api.ppqn())) + } + + sound(name: string) { + if(this.areWeThereYet()) { + const event = this.next() as Pitch|Chord|ZRest; + if(event instanceof Pitch) { + return new Sound(event.asObject(), this.app).sound(name); + } else if(event instanceof Rest) { + return Rest.createRestProxy(event.duration, this.app); + } + } else { + // Not really a rest, but calling for skipping undefined methods + return Rest.createRestProxy(0, this.app); + } + } + + note(value: number|undefined = undefined) { + if(this.areWeThereYet()) { + const event = this.next() as Pitch|Chord|ZRest; + if(event instanceof Pitch) { + console.log(event.asObject()); + const note = new Note(event.asObject(), this.app); + return value ? note.note(value) : note; + } else if(event instanceof ZRest) { + return Rest.createRestProxy(event.duration, this.app); + } + } else { + // Not really a rest, but calling for skipping undefined methods + return Rest.createRestProxy(0, this.app); + } + } + + out = (): void => { + // TODO? + } + + +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 32cc326..2bb49a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -957,7 +957,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lru-cache@^10.0.0, lru-cache@^10.0.1: +lru-cache@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== @@ -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.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.10.tgz#b8c2617f5fc8fb4422f311702785c47b752a920e" - integrity sha512-kaMWRZcsAXXpPFjDoVtS3sQ5bZs+S7t3ejd8+WZV/nc52y/vXe/QcKAjT+jYCHGq8J1WMCITDn6OnVfswqJ8Ig== +zifferjs@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.11.tgz#76cd8f371b65f2176606987cf6fe8e2d156d7d2d" + integrity sha512-JABze3JRHMIzO++4M1EKOJsrG/MzuLMN4ev6XqwJrCGXu7OVyRi3FG4fgA1WAesiuCr/ped/9zHIuodLNMlUOw== dependencies: lru-cache "^10.0.0"