New ziffers player
This commit is contained in:
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
67
src/API.ts
67
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<string, Function>();
|
||||
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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
54
src/Note.ts
54
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);
|
||||
}
|
||||
|
||||
}
|
||||
33
src/Rest.ts
Normal file
33
src/Rest.ts
Normal file
@ -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?
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
66
src/ZPlayer.ts
Normal file
66
src/ZPlayer.ts
Normal file
@ -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?
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
10
yarn.lock
10
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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user