New ziffers player

This commit is contained in:
2023-08-22 22:27:26 +03:00
parent 566d6ce79b
commit a844e0443d
8 changed files with 180 additions and 60 deletions

View File

@ -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"
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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
View 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?
}
}

View File

@ -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
View 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?
}
}

View File

@ -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"