New ziffers player
This commit is contained in:
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?
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user